线程安全
定义:如果有多个线程在同时运行,而这些线程可能会同时运行一段代码。程序每次运行结果和单线程结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全。
线程安全案例
这里通过一个案例来更深一步了解线程的安全问题。
业务:电影院3个窗口卖总共100张票。也就是多线程并发访问同一个数据资源。
public static void main(String[] args) { //创建Runnable接口实现类对象 Tickets t = new Tickets(); //创建三个Thread类对象,传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } public class Tickets implements Runnable { private int ticket = 100; public void run(){ while(true){ if(ticket >0){ System.out.println(Thread.currentThread().getName()+"出售第"+ticket--); } } } }
上面的代码其实是有漏洞的,首先要说线程其实有个特点 ‘从哪跌倒从哪爬起来’,比如上面得代码,如果剩了最后一张票,判断ticket>0进入之后,如果此时这个线程得cpu使用权被抢走了,线程就停在了输出语句这里,没有执行ticket--得操作呢,这时候线程2就会进来,也会进入ticket>0得方法里,那么此时线程2就将输出最后一张票,并且ticket也会被赋值为0,此时由于线程已经进入了这个if里,不需要在判断了,所以线程1也会输出,不过输出就是-1了,很明显就出现了问题了。这个程序就属于线程不安全得程序。上面的代码在测试得时候可能测试很多次都不会出现-1的情况,那是因为代码量少,并且线程熟练少。如果想要看到-1的效果,可以在进入if判断之后加上一个Thread.sleep(10);意思就是让这个线程休眠10毫秒,此时这r个n线程就会让出cpu资源,别的线程就会趁机进来了。
效果:
同步锁
那么对于上面的情况,如何处理呢?
处理思路:我们希望if里面的代码在一个线程执行的时候,其他线程不能够进入,那么这样就可以保证了线程的安全了。
处理办法:java提供的同步技术,同步代码块,锁住。
公式:synchronized(任意对象){线程要操作的共享数据}
同步代码块
对上面的代码修改如下
public static void main(String[] args) { //创建Runnable接口实现类对象 Tickets t = new Tickets(); //创建三个Thread类对象,传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } public class Tickets implements Runnable { private int ticket = 100; private Object object = new Object();//随便定义一个对象 public void run(){ while(true){ //线程共享数据,保证安全,加入同步代码块 synchronized (object) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售第" + ticket--); } } } } }
同步代码块的执行原理
线程遇到同步代码块后,回去判断同步锁是否存在,如果不存在则线程被格挡在外,如果存在,线程获取锁,进入里面的方法并执行,执行完毕之后,离开同步代码块,线程将锁对象还回去(释放锁),优点就是保证了程序的安全性,缺点就是牺牲了程序的运行速度,但是这个是无法避免的。
同步方法
区别于同步代码块的是,synchronized关键字要写在方法上。
public class Tickets implements Runnable { private int ticket = 100; private static int ticket2 = 100; //使用同步方法的方式,不需要这个对象了。那么同步方法还有锁吗?有! // 同步方法中的对象锁,是本类对象引用this,如果方法是静态的static,锁是本类自己+.class //private Object object = new Object(); public void run(){ while(true){ payTicket(); } } //在方法上写上synchronized关键字,也可以达到和同步代码块一样的效果。 public synchronized void payTicket(){ if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售第" + ticket--); } } public static synchronized void payTicket2(){ synchronized (Tickets.class){}//静态方法中的锁是类名+.class if (ticket2 > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售第" + ticket2--); } } }
Lock(jdk1.5后新特性)
Lock实现提供了比使用synchronized方法和语句可获得更广泛得锁定操作。
synchronized方法或语句得使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中,当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
那么我们再对上面的代码进行修改---使用接口Lock,替换同步代码块,实现线程的安全性---lock()获取锁,unlock()释放锁,成员位置创建实现类ReentrantLock
修改如下:
public class Tickets implements Runnable { private int ticket = 100; //在类的成员位置,创建Lock接口的实现类对象 private Lock lock = new ReentrantLock(); public void run() { while (true) { //获取锁 lock.lock(); if (ticket > 0) { try { Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "出售第" + ticket--); } catch (InterruptedException e) { e.printStackTrace(); }finally { //释放锁--释放锁的操作最好放在finally中 lock.unlock(); } } } } }
死锁
定义
就是多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。
死锁出现的前提
必须是多线程的出现同步嵌套。
同步锁
当多个线程同时访问一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行多个线程,在同一时间内只允许一个线程访问共享数据。java中可以使用synchronized关键字来取得一个对象的同步锁。
线程等待与唤醒
在了解线程等待与唤醒机制之前,需要先了解一个概念--线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。这种手段就是等待唤醒机制。
等待唤醒机制所涉及到的方法:
wait():等待,将正在执行的线程释放其执行资格和执行权,并存储到线程池中。
notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
notifyAll():唤醒全部,可以将线程池中所有wait()线程都唤醒。
其实,所谓唤醒的意思就是让线程池中的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。同时这些方法在使用时必须注明所属锁,这样才可以明确出这些方法操作的到底时哪个锁上的线程。
/** * 同时有两个线程,对资源中的变量进行操作 * 线程1:对name和age赋值 * 线程2:对name和age值进行打印 */ public class Resource { public String name; public String sex; public boolean flag = false; } /** * 赋值的时候需要一次是张三一次是李四 */ public class Input implements Runnable { private Resource r; public Input(Resource r){//使用构造方法 保证输入和输出的resouce对象是一个 this.r = r; } public void run(){ int i=0; while(true){ synchronized (r) {//用对象资源锁 而不是this锁 是为了保证输入和输出的锁是一个 以防出现 张三-女的情况 //对标记判断,如果是true,等待 if(r.flag){ try {r.wait();} catch (InterruptedException e) {e.printStackTrace(); }//无论是wait或者notify都需要用锁对象调用,否则会报错 } if (i % 2 == 0) { r.name = "张三"; r.sex = "男"; } else { r.name = "李四"; r.sex = "女"; } //将对方线程唤醒,标记修改为true. r.flag = true; r.notify();//无论是wait或者notify都需要用锁对象调用,否则会报错 } i++; } } } public class Output implements Runnable { private Resource r; public Output(Resource r){//使用构造方法 保证输入和输出的resouce对象是一个 this.r = r; } public void run(){ while (true){ synchronized (r) {//用对象资源锁 而不是this锁 是为了保证输入和输出的锁是一个 以防出现 张三-女的情况 //判断标记,false 等待 if(!r.flag){ try {r.wait(); } catch (InterruptedException e) { e.printStackTrace(); }//无论是wait或者notify都需要用锁对象调用,否则会报错 } System.out.println(r.name + "....." + r.sex); //标记改成false,唤醒对方线程 r.flag = false; r.notify();//无论是wait或者notify都需要用锁对象调用,否则会报错 } } } } //开启输入线程和输出线程,实现赋值和打印值 public static void main(String[] args) { ////这里创建对象 是为了保证输入和输出的resouce对象是一个 Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); //创建三个Thread类对象,传递Runnable接口实现类 Thread tin = new Thread(in); Thread tout = new Thread(out); tin.start(); tout.start(); }
效果:(交错输出,使用flag标记)
参考:
1. 黑马程序员视频:多线程部分
持续更新!!!