多執行緒Demo學習(執行緒的同步,簡單的執行緒通訊)

程式設計師小牧之發表於2020-12-20

一.執行緒的同步

我們使用多執行緒程式設計的一個重要原因在於方便資料的共享。 但是共享就意味著存在安全性問題:如果兩個執行緒同時修改一個資料,該聽誰的?這就引發了同步問題。

1.下面我們用一個銀行存入的例子來演示多執行緒程式設計的非同步的場景
下面一個銀行例項類:

public class Bank {
    private int account = 100;// 假設賬戶的初始金額是100

    public void deposit(int money) {// 向賬戶存錢的方法
        account += money;
    }

    public int getAccount() {// 獲得賬戶金額的方法
        return account;
    }
}

下面是一個銀行的操作任務類:

import javax.swing.JTextArea;

public class Transfer implements Runnable {
    private Bank bank;
    private JTextArea textArea;

    public Transfer(Bank bank, JTextArea textArea) {// 利用構造方法初始化變數
        this.bank = bank;
        this.textArea = textArea;
    }

    public void run() {
        for (int i = 0; i < 100; i++) {// 迴圈10次向賬戶存錢
            bank.deposit(10);// 向賬戶存入10塊錢
            String text = textArea.getText();// 獲得文字域內容
            textArea.setText(text + "賬戶的餘額是:" + bank.getAccount() + "\n");
        }
    }
}

下面是應用場景類:

import java.awt.BorderLayout;
import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.GridLayout;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import java.awt.Font;

public class UnsynchronizedBankFrame extends JFrame {

	private static final long serialVersionUID = 2671056183299397274L;
	private JPanel contentPane;
	private JTextArea thread1TextArea;
	private JTextArea thread2TextArea;

	public static void main(String[] args) {
		try {
			UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
		} catch (Throwable e) {
			e.printStackTrace();
		}
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					UnsynchronizedBankFrame frame = new UnsynchronizedBankFrame();
					frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	public UnsynchronizedBankFrame() {
		setTitle("\u975E\u540C\u6B65\u7684\u6570\u636E\u8BFB\u5199");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(100, 100, 450, 300);
		contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);
		contentPane.setLayout(new BorderLayout(0, 0));

		JPanel buttonPanel = new JPanel();
		contentPane.add(buttonPanel, BorderLayout.SOUTH);

		JButton startButton = new JButton("\u5F00\u59CB\u5B58\u94B1");
		startButton.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
		startButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				do_button_actionPerformed(arg0);
			}
		});
		buttonPanel.add(startButton);

		JPanel processPanel = new JPanel();
		contentPane.add(processPanel, BorderLayout.CENTER);
		processPanel.setLayout(new GridLayout(1, 2, 5, 5));

		JPanel thread1Panel = new JPanel();
		processPanel.add(thread1Panel);
		thread1Panel.setLayout(new BorderLayout(0, 0));

		JLabel thread1Label = new JLabel("\u4E00\u53F7\u7EBF\u7A0B");
		thread1Label.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
		thread1Label.setHorizontalAlignment(SwingConstants.CENTER);
		thread1Panel.add(thread1Label, BorderLayout.NORTH);

		JScrollPane thread1ScrollPane = new JScrollPane();
		thread1Panel.add(thread1ScrollPane, BorderLayout.CENTER);

		thread1TextArea = new JTextArea();
		thread1TextArea.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
		thread1ScrollPane.setViewportView(thread1TextArea);

		JPanel thread2Panel = new JPanel();
		processPanel.add(thread2Panel);
		thread2Panel.setLayout(new BorderLayout(0, 0));

		JLabel thread2Label = new JLabel("\u4E8C\u53F7\u7EBF\u7A0B");
		thread2Label.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
		thread2Label.setHorizontalAlignment(SwingConstants.CENTER);
		thread2Panel.add(thread2Label, BorderLayout.NORTH);

		JScrollPane thread2ScrollPane = new JScrollPane();
		thread2Panel.add(thread2ScrollPane, BorderLayout.CENTER);

		thread2TextArea = new JTextArea();
		thread2TextArea.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
		thread2ScrollPane.setViewportView(thread2TextArea);
	}

	protected void do_button_actionPerformed(ActionEvent arg0) {
		Bank bank = new Bank();
		Thread thread1 = new Thread(new Transfer(bank, thread1TextArea));
		thread1.start();
		Thread thread2 = new Thread(new Transfer(bank, thread2TextArea));
		thread2.start();
	}
}

我們通過檢視上面的程式碼可以知道的是,此程式會建立兩個執行緒,在沒有任何防護措施的情況下對銀行賬戶同時進行存錢,每個執行緒存入100塊,如果沒有差錯的話,銀行賬戶的餘額最後應為2100,好,下面我們執行一下看看結果:
在這裡插入圖片描述
很明顯,發生了實際與期望不一致的情況(由於同步問題存在一定的概率性,執行結果如果沒有問題可以多試幾次)。
所以,如果我們想要避免這種問題的發生,就需要通過一些鎖或其他的同步策略來解決。

2.使用內建鎖來解決非同步問題:
如何解決上面的問題呢? 其實我們可以發現兩個執行緒的關鍵操作在於:

 bank.deposit(10);// 向賬戶存入10塊錢
 
 public void deposit(int money) {// 向賬戶存錢的方法
        account += money;
    }

在這裡兩個執行緒呼叫的是同一個物件的同一個方法,這個deposit方法就相當於一個臨界資源,我們需要對它採取一定的措施來解決方法競爭問題:
1)最簡單的方法就是給它加一個內建鎖:

 public synchronized void deposit(int money) {// 向賬戶存錢的方法
        account += money;
    }

這樣的話它每次只能被一個執行緒擁有,也就是說,不會同時有兩個執行緒執行它。
下面我們執行下更改後的程式:
在這裡插入圖片描述
執行結果是我們期望的。

2)我們也可以通過程式碼塊來解決(比前一種方法更優):

 public void deposit(int money) {// 向賬戶存錢的方法
       synchronized(this){
        account += money;
        }
    }

下面看下效果:
在這裡插入圖片描述
是我們期望的結果。

注意:volatile它只提供可見性(每個執行緒都保證讀取的是最新的值),並不提供互斥性。 所以它不可以解決上面的問題。

3)使用顯示鎖解決

 private Lock lock = new ReentrantLock();
 public void deposit(int money) {// 向賬戶存錢的方法
           lock.lock();
           try {
               account += money;
           }finally{
               lock.unlock();
           }
        
    }

執行效果:
在這裡插入圖片描述

二.簡單的執行緒通訊

使用多執行緒程式設計的一個重要原因就是執行緒間通訊的代價比較小。下面的例子演示了簡單的執行緒通訊:

package Dome.exa179;

import java.awt.BorderLayout;
import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.JButton;
import java.awt.GridLayout;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.UIManager;
import java.awt.Font;

public class TransactionFrame extends JFrame {

    private static final long serialVersionUID = -4239009401384819805L;
    private JPanel contentPane;
    private JTextArea senderTextArea;
    private JTextArea receiverTextArea;

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Throwable e) {
            e.printStackTrace();
        }
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    TransactionFrame frame = new TransactionFrame();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TransactionFrame() {
        setTitle("\u7B80\u5355\u7684\u7EBF\u7A0B\u901A\u4FE1");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        setContentPane(contentPane);
        contentPane.setLayout(new BorderLayout(0, 0));

        JPanel buttonPanel = new JPanel();
        contentPane.add(buttonPanel, BorderLayout.SOUTH);

        JButton button = new JButton("\u5F00\u59CB\u4EA4\u6613");
        button.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                do_button_actionPerformed(arg0);
            }
        });
        buttonPanel.add(button);

        JPanel transactionPanel = new JPanel();
        contentPane.add(transactionPanel, BorderLayout.CENTER);
        transactionPanel.setLayout(new GridLayout(1, 2, 5, 5));

        JPanel senderPanel = new JPanel();
        transactionPanel.add(senderPanel);
        senderPanel.setLayout(new BorderLayout(0, 0));

        JLabel senderLabel = new JLabel("\u5356\u5BB6");
        senderLabel.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
        senderLabel.setHorizontalAlignment(SwingConstants.CENTER);
        senderPanel.add(senderLabel, BorderLayout.NORTH);

        JScrollPane senderScrollPane = new JScrollPane();
        senderPanel.add(senderScrollPane, BorderLayout.CENTER);

        senderTextArea = new JTextArea();
        senderTextArea.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
        senderScrollPane.setViewportView(senderTextArea);

        JPanel receiverPanel = new JPanel();
        transactionPanel.add(receiverPanel);
        receiverPanel.setLayout(new BorderLayout(0, 0));

        JLabel receiverLabel = new JLabel("\u4E70\u5BB6");
        receiverLabel.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
        receiverLabel.setHorizontalAlignment(SwingConstants.CENTER);
        receiverPanel.add(receiverLabel, BorderLayout.NORTH);

        JScrollPane receiverScrollPane = new JScrollPane();
        receiverPanel.add(receiverScrollPane, BorderLayout.CENTER);

        receiverTextArea = new JTextArea();
        receiverTextArea.setFont(new Font("微軟雅黑", Font.PLAIN, 16));
        receiverScrollPane.setViewportView(receiverTextArea);
    }

    protected void do_button_actionPerformed(ActionEvent arg0) {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
        Thread st = new Thread(sender);
        Thread rt = new Thread(receiver);
        st.start();
        rt.start();
    }

    private class Sender implements Runnable {
        private String[] products = { "《Java程式設計詞典》", "《Java範例大全》", "《視訊學Java程式設計》", "《細說Java》", "《Java開發實戰寶典》" };// 模擬商品列表
        private volatile String product;// 儲存一個商品名稱
        private volatile boolean isValid;// 儲存賣家是否傳送商品的狀態

        public boolean isIsValid() {// 讀取狀態
            return isValid;
        }

        public void setIsValid(boolean isValid) {// 設定狀態
            this.isValid = isValid;
        }

        public String getProduct() {// 獲得商品
            return product;
        }

        public void run() {
            for (int i = 0; i < 5; i++) {// 向買家傳送5次商品
                while (isValid) {// 如果已經傳送商品就進入等待狀態,等待買家接收
                    Thread.yield();
                }
                product = products[i];// 獲得一件商品
                String text = senderTextArea.getText();// 獲得賣家文字域資訊
                senderTextArea.setText(text + "傳送:" + product + "\n");// 更新賣家文字域資訊
                try {
                    Thread.sleep(100);// 當前執行緒休眠0.1秒實現傳送的效果
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isValid = true;// 將狀態設定為已經傳送商品
            }
        }
    }

    private class Receiver implements Runnable {
        private Sender sender;// 建立一個對傳送者的引用

        public Receiver(Sender sender) {// 利用構造方法初始化傳送者引用
            this.sender = sender;
        }

        public void run() {
            for (int i = 0; i < 5; i++) {// 接收5次商品
                while (!sender.isIsValid()) {// 如果傳送者沒有傳送商品就進行等待
                    Thread.yield();
                }
                String text = receiverTextArea.getText();// 獲得賣家文字域資訊
                // 更新賣家文字域資訊
                receiverTextArea.setText(text + "收到:" + sender.getProduct() + "\n");
                try {
                    Thread.sleep(1000);// 執行緒休眠1秒實現動態傳送的效果
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sender.setIsValid(false);// 設定賣家傳送商品的狀態為未傳送,這樣賣家就可以繼續傳送商品
            }
        }
    }

}

下面我們看一下執行效果:
在這裡插入圖片描述

同樣我們從程式碼中提取出核心部分(三個):
1)傳送者任務:

private class Sender implements Runnable {
        private String[] products = { "《Java程式設計詞典》", "《Java範例大全》", "《視訊學Java程式設計》", "《細說Java》", "《Java開發實戰寶典》" };// 模擬商品列表
        private volatile String product;// 儲存一個商品名稱
        private volatile boolean isValid;// 儲存賣家是否傳送商品的狀態
        public boolean isIsValid() {// 讀取狀態
            return isValid;
        }
        public void setIsValid(boolean isValid) {// 設定狀態
            this.isValid = isValid;
        }
        public String getProduct() {// 獲得商品
            return product;
        }
        public void run() {
            for (int i = 0; i < 5; i++) {// 向買家傳送5次商品
                while (isValid) {// 如果已經傳送商品就進入等待狀態,等待買家接收
                    Thread.yield();
                }
                product = products[i];// 獲得一件商品
                String text = senderTextArea.getText();// 獲得賣家文字域資訊
                senderTextArea.setText(text + "傳送:" + product + "\n");// 更新賣家文字域資訊
                try {
                    Thread.sleep(100);// 當前執行緒休眠0.1秒實現傳送的效果
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isValid = true;// 將狀態設定為已經傳送商品
            }
        }
    }

我們可以看到它使用了一個狀態變數來幫助通訊,它的執行邏輯如下:
首先第一次迴圈獲得商品陣列中的一個物品放入一個快取字串中,然後將狀態變數設為true,然後進入第二次迴圈,第二次迴圈的while執行此執行緒進入等待狀態。等待另一個執行緒執行。

2)接收者任務:

private class Receiver implements Runnable {
        private Sender sender;// 建立一個對傳送者的引用

        public Receiver(Sender sender) {// 利用構造方法初始化傳送者引用
            this.sender = sender;
        }

        public void run() {
            for (int i = 0; i < 5; i++) {// 接收5次商品
                while (!sender.isIsValid()) {// 如果傳送者沒有傳送商品就進行等待
                    Thread.yield();
                }
                String text = receiverTextArea.getText();// 獲得賣家文字域資訊
                // 更新賣家文字域資訊
                receiverTextArea.setText(text + "收到:" + sender.getProduct() + "\n");
                try {
                    Thread.sleep(1000);// 執行緒休眠1秒實現動態傳送的效果
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sender.setIsValid(false);// 設定賣家傳送商品的狀態為未傳送,這樣賣家就可以繼續傳送商品
            }
        }
    }

裡面組合了傳送者,這個是實現通訊的關鍵。它的執行邏輯是這樣的:
首先它進行第一次迴圈進入等待狀態,然後傳送者執行緒進入了等待狀態後,它繼續執行,然後將狀態變數設為false,然後第二次迴圈進入等待狀態。 依次類推。

3)應用程式碼:

 protected void do_button_actionPerformed(ActionEvent arg0) {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
        Thread st = new Thread(sender);
        Thread rt = new Thread(receiver);
        st.start();
        rt.start();
    }

相關文章