一.线程的同步
我们使用多线程编程的一个重要原因在于方便数据的共享。 但是共享就意味着存在安全性问题:如果两个线程同时修改一个数据,该听谁的?这就引发了同步问题。
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();
}