该章主要围绕同步来介绍实现多线程由于共享资源出现的安全问题,使用同步解决安全问题,由同步而产生的死锁问题。三个方面来介绍。
举个例子:比如我卖100张票,通过三个窗口卖,那么可以用如下代码实现。
public class MultipleThread {
public static void main(String[] args) {
SingleThread st = new SingleThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
t1.start();
t2.start();
t3.start();
}
}
class SingleThread implements Runnable {
private int totalTicket = 100;
@Override
public void run() {
while (true) {
if (totalTicket > 0)
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " sell ticket " + totalTicket--);
}
}
}
}
/*
Thread-0 sell ticket 1
Thread-2 sell ticket 0
Thread-1 sell ticket -1*/
这个程序可以通过三个窗口卖完100个票,但是可能出现问题是,比如我们卖到只剩一张票时,一个窗口卖了这张的时候,还没卖完,另一个窗口查系统的时候看到还剩一张,就也卖这张票,这时就可能出现一张票被多次售卖了。因为有共享数据,这就多线程出现了安全问题。
多线程安全问题原因:当多条线程同时操作一个共享数据时,一个线程还没执行完,另一个线程就也进来执行共享数据,就可能会出现共享数据错误。
解决方法:一个线程操作该共享数据时,比如ticket= 1,其他线程不能进来操作这个ticket,当该线程执行完之后,其他线程才能执行。
synchronized(对象)
{
需要同步的代码(操作共享数据的代码)
}
public class MultipleThread {
public static void main(String[] args) {
SingleThread st = new SingleThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
t1.start();
t2.start();
t3.start();
}
}
class SingleThread implements Runnable {
//共享变量
private int totalTicket = 100;
//共享对象
private Object o = new Object();
@Override
public void run() {
while (true) {
synchronized (o) {//同步必须是同一个对象
if (totalTicket > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " sell ticket " + totalTicket--);
}
}
}
}
}
同步的好处:解决了多线程安全问题。
弊端:每次都需要判断锁,会消耗资源
介绍:银行有个总账,不同的人来存钱后会计入总账。假设有三个客户,每人每次来存100元,存三次,总账记录900元。
实现代码:
public class MultipleThread {
public static void main(String[] args) {
Customer customer = new Customer();
Thread t1 = new Thread(customer);
Thread t2 = new Thread(customer);
Thread t3 = new Thread(customer);
t1.start();//客户1跑线程1
t2.start();
t3.start();
}
}
//银行总账类
class Bank {
private int moneySum = 0;
void addMoney(int n) {
moneySum = moneySum + n;
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println("sum = " + moneySum);
}
}
//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
//此对象为多线程共享对象
private Bank bank = new Bank();
//存钱,存三次
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " save money:");
bank.addMoney(100);
}
}
}
/*
Thread-0 save money:
Thread-1 save money:
Thread-2 save money:
sum = 300
Thread-0 save money:
sum = 300
Thread-1 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-0 save money:
sum = 800
Thread-2 save money:
sum = 900
sum = 900
sum = 900
*/
这时我们发现,操作共享数据出现问题,打印出了错误的银行总账。这是因为我们操作共享数据时没有进行同步,也就是说多个线程同时操作一个共享变量。
解决:
**先明确共享数据,然后明确多线程代码中哪些语句是操作共享数据的。**如果弄错了同步的语句块,可能会造成多线程编程串行程序而顺序执行就不是并发了。
我们发现
private Bank bank = new Bank();
private int moneySum = 0;
是共享数据
操作共享数据的语句为:
bank.addMoney(100);
以及他内部语句
然后我们可以通过同步代码块和同步函数来进行解决。
同步代码块中的对象是唯一的,他称为一个同步锁(互斥锁),当一个线程持有该锁,其他线程不能够使用该锁。也就是通过这个唯一锁每次持有该锁的线程才能执行代码。一个线程执行完之后释放该锁,其他线程来拿到锁继续执行。
synchronized(对象)
{
需要同步的代码(操作共享数据的代码)
}
public class MultipleThread {
public static void main(String[] args) {
Customer customer = new Customer();
Thread t1 = new Thread(customer);
Thread t2 = new Thread(customer);
Thread t3 = new Thread(customer);
t1.start();//客户1跑线程1
t2.start();
t3.start();
}
}
//银行总账类
class Bank {
private int moneySum = 0;
void addMoney(int n) {
moneySum = moneySum + n;
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println("sum = " + moneySum);
}
}
//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
//此对象为多线程共享对象
private Bank bank = new Bank();
Object O = new Object();
//存钱,存三次
@Override
public void run() {
for (int i = 0; i < 3; i++) {
synchronized (O) {
System.out.println(Thread.currentThread().getName() + " save money:");
bank.addMoney(100);
}
}
}
}
/*
Thread-0 save money:
sum = 100
Thread-0 save money:
sum = 200
Thread-0 save money:
sum = 300
Thread-2 save money:
sum = 400
Thread-2 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-1 save money:
sum = 800
Thread-1 save money:
sum = 900
*/
我们还可以直接将bank.addMoney
方法定义为同步方法,这样我们就不需要同步代码块,不用创建同步对象锁了。他的写法更简洁。同步函数需要被对象调用,所以同步函数使用的锁就是this对象。在下面的实例中this指向的是唯一的customer对象,所以锁是相同的对象。
public class MultipleThread {
public static void main(String[] args) {
Customer customer = new Customer();
Thread t1 = new Thread(customer);
Thread t2 = new Thread(customer);
Thread t3 = new Thread(customer);
t1.start();//客户1跑线程1
t2.start();
t3.start();
}
}
//银行总账类
class Bank {
private int moneySum = 0;
//同步方法
synchronized void addMoney(int n) {
System.out.println(Thread.currentThread().getName() + " save money:");
moneySum = moneySum + n;
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println("sum = " + moneySum);
}
}
//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
//此对象为多线程共享对象
private Bank bank = new Bank();
//存钱,存三次
@Override
public void run() {
for (int i = 0; i < 3; i++) {
bank.addMoney(100);
}
}
}
/*
Thread-0 save money:
sum = 100
Thread-0 save money:
sum = 200
Thread-0 save money:
sum = 300
Thread-2 save money:
sum = 400
Thread-2 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-1 save money:
sum = 800
Thread-1 save money:
sum = 900
*/
synchronized(Bank.class)
{
需要同步的代码(操作共享数据的代码)
}
单例设计模式实际上是指不允许直接创建该类对象,把构造方法设置为私有,是通过调用该类方法来创建对象实例。
饿汉式:
class Single{
private static final Single s = new Single();
private Single(){}
public static Single getInstance(){
return s;
}
}
懒汉式:使用延迟加载,但是由于共享变量可能出现安全问题,通过加同步块来解决,同步块的锁为该类的字节码文件对象。
class Single {
private static Single s = null;
private Single() {
}
public static Single getInstance() {
if (s == null) {
synchronized (Single.class) {
if (s == null)
s = new Single();
}
}
return s;
}
}
同步可能产生死锁。
一个线程有一个锁,不放自己的锁要到另一个线程执行,而另一个线程也有一个锁不放,两者要相互访问但是都不放自己的锁就会产生死锁问题。当出现死锁程序会卡死。
通常由于同步中嵌套同步产生。
下面实例产生死锁:
class ProductThreadA implements Runnable {
@Override
public void run() {
//这里一定要让线程睡一会儿来模拟处理数据 ,要不然的话死锁的现象不会那么的明显.
//这里就是同步语句块嵌套,首先获得对象锁lockA,然后执行一些代码,随后我们需要对象锁lockB去执行另外一些代码.
synchronized (LockTest.lockA) {
System.out.println("ThreadA lock lockA");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LockTest.lockB) {
System.out.println("ThreadA lock lockB");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ProductThreadB implements Runnable {
//我们线程B的拿锁顺序相反,我们首先需要对象锁lockB,然后需要对象锁lockA.
@Override
public void run() {
synchronized (LockTest.lockB) {
System.out.println("ThreadB lock lockB");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LockTest.lockA) {
System.out.println("ThreadB lock lockA");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class LockTest {
//首先我们先定义两个final的对象锁.可以看做是共有的资源.
static final Object lockA = new Object();
static final Object lockB = new Object();
public static void main(String[] args) {
//运行两个线程
Thread threadA = new Thread(new ProductThreadA());
Thread threadB = new Thread(new ProductThreadB());
threadA.start();
threadB.start();
}
}
/*
ThreadA lock lockA
ThreadB lock lockB
*/
我们可以看到当一个线程持有锁A而要去拿锁B,而另一个线程持有锁B要去拿锁A,两者都还没有释放锁,这时就会造成死锁,程序会卡死在这里。
所以避免死锁是非常重要的,如果一个线程每次只能获得一个锁,那么就不会产生锁顺序的死锁。尽量保证一个线程每次仅使用一个锁。
如果必须要使用两个锁,下面介绍两种方法来避免死锁
如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下:
而我们可以改一下获取锁的顺序:
这样一个线程总会等待另一个线程释放同一个锁时候才执行,这就不会出现死锁情况。
当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,那就死锁了。
我们可以使用Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
时序图如下: