一 概述
多线程在效率上能带给我们一些提升,但是也带来了一些其它的问题,这些问题如果不解决,我们根本无法保证线程的运算结果是正确的.
那么,这个时候使用多线程根本不存在任何意义.
带来的问题:
[1] 多线程共享一个资源---造成资源状态不一致
[2] 多线程的执行顺序 ,线程一旦运行起来,我们能无法控制线程到底是怎么运行的.
对此,我们就需要使用线程的同步和线程的通信来完成上面问题的解决.
二 . 线程间的资源共享
当多个线程之间共享资源的时候,就可能出现资源使用上的问题.
这里需要指出的是可能会出现问题,不是一定会出现问题.
我们现在所要做的第一步就是明白什么样的情况下会出现问题.
问题的解决就是加锁,
如果在根本没有资源共享出现问题的地方加上锁,除了影响效率之外根本没有任何的好处.
三 .资源共享何时出现问题
问题1 : 当多个线程都读取一个资源问题,但是不允许对资源问题进行任何修改.
此时根本不需要进行加锁操作,因为无论何时,资源的状态都是一致性的.
问题2 : 当多个线程使用一个资源问题,可以对其进行修改操作的时候.
这个时候,我们就需要进行加锁.
加锁的原因只有一个: 保证资源的一致性状态.
上面的一致性比较难理解:
一致性就是说,资源的状态一定需要是一个稳定状态.当一个线程对资源进行修改的时候,资源的状态在修改过程中就不是稳定,那么其他线程就不应该看到这种状态.
四 .测试代码
我们选用一个非常经典的例子:
public class Ticket { //表示100张票 private static int num = 100; public static void consume() { for(;;) { if(num > 0) { System.out.println(Thread.currentThread().getName()+": 卖出第"+(num--)+"张票"); }else return ; } } public static void main(String[] args) { new Thread("thread1") { @Override public void run() { consume(); } }.start(); new Thread("thread2") { @Override public void run() { consume(); } }.start(); } }
在上面的例子中,我们开启两个线程再卖票.我们的共享数据就是num这个参数.
那上面的例子有问题没有呢?
我们稍加改造一下:
public static void consume() { for(;;) { if(num > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+": 卖出第"+(num--)+"张票"); } catch (InterruptedException e) { e.printStackTrace(); } }else return ; } }
仅仅是在consume方法之中,在打印之前让线程休眠一下.
我们可以看到结果:
现在出现了0张票的概念,也就是说我们上面的例子中存在并发的问题.
五 .问题的解决
由于资源共享问题而出现的并发问题,我们使用加锁就能完成.
在java之中,加锁的方式有很多.现在我们只说最为简单的几种,在后面我们会说并法宝为我们提供的一系列的锁机制.
[1] 方式1 : 同步代码块
我们可以使用synchronized 关键词来完成.
public static void consume() { for(;;) { synchronized (Ticket.class) { if (num > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + ": 卖出第" + (num--) + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } else return; } } }
我们将出现问题的代码加入到同步代码块之中,那么着一段代码就仅仅允许获取到锁的线程运行,也就是说不会发生资源同时被多个线程争夺的问题了.
注意 : 我们使用的锁必须说唯一的锁,否则还是会出现问题的.
[2]方式二 . 使用同步方法
public synchronized static void consume() { for(;;) { if (num > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + ": 卖出第" + (num--) + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } else return; } }
在这里我们使用的是同步方法,其实和同步方法一致,也是对整个方法进行加锁,这个方法仅仅允许一个线程访问.
六 .同步方法和同步代码块
其实本质上都是一样的东西,
我们如何执行上面的同步方法的时候,我们会发现我们的并发只有一个线程在工作.
为什么呢 ? 原因就是一个线程一旦抢占了锁之后,就把所有的票卖没了 .
现在我们换一个方式:
public class Ticket { // 表示100张票 private static int num = 100; public synchronized static void consume() { if (num > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + ": 卖出第" + (num--) + "张票"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { new Thread("thread1") { @Override public void run() { for(;;) consume(); } }.start(); new Thread("thread2") { @Override public void run() { for(;;) consume(); } }.start(); } }
我们现在的线程逻辑单元仅仅是卖一张票了.
那么我们又可以看到线程的交替运行了.
总结 :
[1]静态同步方法的锁是class
[2]普通方法加锁的对象是this.