2.1 线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
测试类
1 /* 2 * 多线程并发访问同一个数据资源 3 * 3个线程,对一个票资源,出售 4 */ 5 public class ThreadDemo { 6 public static void main(String[] args) { 7 //创建Runnable接口实现类对象 8 Tickets t = new Tickets(); 9 //创建3个Thread类对象,传递Runnable接 口实现类10 Thread t0 = new Thread(t);11 Thread t1 = new Thread(t);12 Thread t2 = new Thread(t);13 14 t0.start();t1.start();t2.start();15 16 }17 }
模拟票
1 public class Ticket implements Runnable { 2 //共100票 3 int ticket = 100; 4 5 @Override 6 public void run() { 7 //模拟卖票 8 while(true){ 9 if (ticket > 0) {10 //模拟选坐的操作11 try {12 Thread.sleep(1);13 } catch (InterruptedException e) {14 e.printStackTrace();15 }16 System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);17 }18 }19 }
运行结果发现:上面程序出现了问题
- 票出现了重复的票
- 错误的票 0、-1
其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
2.2 线程同步(线程安全处理Synchronized)
java中提供了线程同步机制,它能够解决上述的线程安全问题。
线程同步的方式有两种:
- 方式1:同步代码块
- 方式2:同步方法
2.2.1 同步代码块
同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) { 可能会产生线程安全问题的代码}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:
1 /* 2 * 通过线程休眠,出现安全问题 3 * 解决安全问题,Java程序,提供技术,同步技术 4 * 公式: 5 * synchronized(任意对象){ 6 * 线程要操作的共享数据 7 * } 8 * 同步代码块 9 */10 public class Tickets implements Runnable{11 12 //定义出售的票源13 private int ticket = 100;14 private Object obj = new Object();15 16 public void run(){17 while(true){18 //线程共享数据,保证安全,加入同步代码块19 synchronized(obj){20 //对票数判断,大于0,可以出售,变量--操作21 if( ticket > 0){22 try{23 Thread.sleep(1);24 }catch(Exception ex){}25 finally {26 System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);27 }28 29 }30 }31 }32 }33 }
当使用了同步代码块后,上述的线程的安全问题,解决了。
2.2.2 同步方法
同步方法:在方法声明上加上synchronized
public synchronized void method(){ 可能会产生线程安全问题的代码}
同步方法中的锁对象是 this
使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:
1 package cn.itcast.demo1; 2 /* 3 * 采用同步方法形式,解决线程的安全问题 4 * 好处: 代码简洁 5 * 将线程共享数据,和同步,抽取到一个方法中 6 * 在方法的声明上,加入同步关键字 7 * 8 * 问题: 9 * 同步方法有锁吗,肯定有,同步方法中的对象锁,是本类对象引用 this10 * 如果方法是静态的呢,同步有锁吗,绝对不是this11 * 锁是本类自己.class 属性12 * 静态方法,同步锁,是本类类名.class属性13 */14 public class Tickets implements Runnable{15 16 //定义出售的票源17 private int ticket = 100;18 19 public void run(){20 while(true){21 payTicket();22 }23 }24 25 public synchronized void payTicket(){ 26 if( ticket > 0){27 try{28 Thread.sleep(10);29 }catch(Exception ex){}30 System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);31 }32 33 }34 }
静态同步方法: 在方法声明上加上static synchronized
public static synchronized void method(){ 可能会产生线程安全问题的代码}
静态同步方法中的锁对象是 类名.class
2.3 Lock接口
查阅API,查阅Lock接口描述,Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。
Lock接口中的常用方法
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 /* 5 * 使用JDK1.5 的接口Lock,替换同步代码块,实现线程的安全性 6 * Lock接口方法: 7 * lock() 获取锁 8 * unlock()释放锁 9 * 实现类ReentrantLock10 */11 public class Tickets implements Runnable{12 13 //定义出售的票源14 private int ticket = 100;15 //在类的成员位置,创建Lock接口的实现类对象16 private Lock lock = new ReentrantLock();17 18 public void run(){19 while(true){20 //调用Lock接口方法lock获取锁21 lock.lock();22 //对票数判断,大于0,可以出售,变量--操作23 if( ticket > 0){24 try{25 Thread.sleep(10);26 System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);27 }catch(Exception ex){28 29 }finally{30 //释放锁,调用Lock接口方法unlock31 lock.unlock();32 }33 }34 35 36 }37 }38 }
2.4 死锁
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
synchronzied(A锁){ synchronized(B锁){ }}
我们进行下死锁情况的代码演示:
定义锁a对象类
public class LockA { private LockA(){} public static final LockA locka = new LockA();}
定义锁b对象类
public class LockB { private LockB(){} public static final LockB lockb = new LockB();}
线程任务类
1 public class DeadLock implements Runnable{ 2 private int i = 0; 3 public void run(){ 4 while(true){ 5 if(i%2==0){ 6 //先进入A同步,再进入B同步 7 synchronized(LockA.locka){ 8 System.out.println("if...locka"); 9 synchronized(LockB.lockb){10 System.out.println("if...lockb");11 }12 }13 }else{14 //先进入B同步,再进入A同步15 synchronized(LockB.lockb){16 System.out.println("else...lockb");17 synchronized(LockA.locka){18 System.out.println("else...locka");19 }20 }21 }22 i++;23 }24 }25 }
测试类
1 public class DeadLockDemo {2 public static void main(String[] args) {3 DeadLock dead = new DeadLock();4 Thread t0 = new Thread(dead);5 Thread t1 = new Thread(dead);6 t0.start();7 t1.start();8 }9 }
2.5 等待唤醒机制
在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:
- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
- notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
接下里,我们先从一个简单的示例入手:
如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:
- 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
- 当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。
下面代码,模拟等待唤醒机制的实现:
模拟资源类
1 /* 2 * 定义资源类,有2个成员变量 3 * name,sex 4 * 同时有2个线程,对资源中的变量操作 5 * 1个对name,age赋值 6 * 2个对name,age做变量的输出打印 7 */ 8 public class Resource { 9 public String name;10 public String sex;11 public boolean flag = false;12 }
输入类
/* * 输入的线程,对资源对象Resource中成员变量赋值 * 一次赋值 张三,男 * 下一次赋值 lisi,nv */public class Input implements Runnable { private Resource r ; public Input(Resource r){ this.r = r; } public void run() { int i = 0 ; while(true){ synchronized(r){ //标记是true,等待 if(r.flag){ try{r.wait();}catch(Exception ex){} } if(i%2==0){ r.name = "张三"; r.sex = "男"; }else{ r.name = "lisi"; r.sex = "nv"; } //将对方线程唤醒,标记改为true r.flag = true; r.notify(); } i++; } }}
输出类
1 /* 2 * 输出线程,对资源对象Resource中成员变量,输出值 3 */ 4 public class Output implements Runnable { 5 private Resource r ; 6 7 public Output(Resource r){ 8 this.r = r; 9 }10 public void run() {11 while(true){12 synchronized(r){ 13 //判断标记,是false,等待14 if(!r.flag){15 try{r.wait();}catch(Exception ex){}16 }17 System.out.println(r.name+".."+r.sex);18 //标记改成false,唤醒对方线程19 r.flag = false;20 r.notify();21 }22 }23 }24 25 }
测试类
/* * 开启输入线程和输出线程,实现赋值和打印值 */public class ThreadDemo{ public static void main(String[] args) { Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread tin = new Thread(in); Thread tout = new Thread(out); tin.start(); tout.start(); }}
总结
- 同步锁
多个线程想保证线程安全,必须要使用同一个锁对象
①同步代码块
1 synchronized (锁对象){ 2 可能产生线程安全问题的代码 3 }
同步代码块的锁对象可以是任意的对象
②同步方法
1 public synchronized void method()2 可能产生线程安全问题的代码3 }
同步方法中的锁对象是 this
- 静态同步方法
1 public synchronized void method()2 可能产生线程安全问题的代码3 }
静态同步方法中的锁对象是 类名.class
l 多线程有几种实现方案,分别是哪几种?
- 继承Thread类
- 实现Runnable接口
- 通过线程池,实现Callable接口
l 同步有几种方式,分别是什么?
- 同步代码块
- 同步方法、
- 静态同步方法
l 启动一个线程是run()还是start()?它们的区别?
启动一个线程是start()
区别:
start: 启动线程,并调用线程中的run()方法
run : 执行该线程对象要执行的任务
l sleep()和wait()方法的区别
sleep: 不释放锁对象, 释放CPU使用权
在休眠的时间内,不能唤醒
wait(): 释放锁对象, 释放CPU使用权
在等待的时间内,能唤醒
l 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
锁对象可以是任意类型的对象