当多线程共同读写共享资源的时候,为了达到线程安全的目的,从而有了有锁编程。先从基本概念谈起:
什么叫多线程?
一段程序加载到内存,引导启动后,操作系统会给该程序创建一个以PID为唯一标示的进程。进程简而言之就是这段程序在操作系统之上的一次动态执行(从加载到内存,引导启动,运行,到结束)。进程包含很丰富的信息(PID,进程控制单元,进程空间,分配的内存资源,以及操作系统调度得到的CPU资源,还有别的资源),同时也是比较重量级的。而一个进程中为了让CPU以及IO资源使用的更到位,从而有了线程。所以线程诞生的目的:尽量吃满资源(CPU,IO),或者换句话说就是尽量更充分的使用计算机资源,从而达到有更高的吞吐量。一个进程内部的线程可以共享该进程的资源,同时线程也有自己的TID,线程栈空间,线程间公用的内存空间。
好,说完上面基本信息,所以将你的程序设计成多线程程序就水到渠成了。
什么叫共享资源?
共享资源可以是一个变量,可以是一个文件,也可以是一个复杂的数据结构。因为某团厕所坑位比较紧张,所以用WC坑位举栗子。
正常情况下一个坑位同时只能有一个人占用,这个是不允许同个人同时享用的。所以在这里坑位就可以理解成一个共享资源。那多个人(多线程)想使用一个坑位,怎么办呢?很简单给每个坑位上一把锁,有个这个锁就可以保证坑位这个共享资源同时只被一个人使用。
通俗的栗子讲完,那进入操作系统的世界中,如何理解共享资源?下面将两个计算机世界的例子
线程间状态变量可见的栗子
两个线程共享isRunning变量,第一个线程对isRunning有修改成false,但是第二个线程一直读取不到该变量的修改。
public class VolatileExampleV2 { boolean isRunning = true; long gap = 50; public static void main(String[] args) { new VolatileExampleV2().test(); } private void test() { new Thread(new Runnable() { @Override public void run() { doSomeThing(2000); isRunning = false; System.out.println("first thread currentTime:" + System.currentTimeMillis()); doSomeThing(500); } }).start(); new Thread(new Runnable() { @Override public void run() { while (isRunning) { } System.out.println("Second Thread currentTime:" + System.currentTimeMillis()); } }).start(); } private static void doSomeThing(long time) { long sum = 0; long size = time * 100000; for (int i = 0; i < size; i++) { sum += i; } } } ###########只打印一行内容,程序一直在第二个线程跑,并未结束 first thread currentTime:1442215575596
通过volatile关键字就可以控制线程间变量的可见性,栗子如下:
public class VolatileExampleV2 { volatile boolean isRunning = true; long gap = 50; public static void main(String[] args) { new VolatileExampleV2().test(); } private void test() { new Thread(new Runnable() { @Override public void run() { doSomeThing(2000); isRunning = false; System.out.println("first thread currentTime:" + System.currentTimeMillis()); doSomeThing(500); } }).start(); new Thread(new Runnable() { @Override public void run() { while (isRunning) { } System.out.println("Second Thread currentTime:" + System.currentTimeMillis()); } }).start(); } private static void doSomeThing(long time) { long sum = 0; long size = time * 100000; for (int i = 0; i < size; i++) { sum += i; } } }
#####################这次两个线程都能跑完,第一个线程对共享变量的修改,被第二个线程看见了。
Second Thread currentTime:1442215856761
first thread currentTime:1442215856761
PS:想写这个栗子很不容易,因为必须确保第二个线程每次读取共享状态变量是从该CPU的独立缓存中读取,不能从内存中读取,不然就实现不了线程可见性的例子。
读写修改共享参数的栗子
public class SynchronizedExample { private Count wangxin = new Count(); public static void main(String[] args) { new SynchronizedExample().test(); } private void test() { System.out.println("user count:" + wangxin); List<Thread> threadList = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threadList.add(new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } saveMoeny(); } })); } for (Thread each : threadList) { each.start(); } try { Thread.currentThread().join(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("user count:" + wangxin); } public void saveMoeny() { int moeny = wangxin.getMoeny(); wangxin.setMoeny(moeny + 100); } } public class Count { private int moeny = 100; public int getMoeny() { return moeny; } public void setMoeny(int moeny) { this.moeny = moeny; } @Override public String toString() { return "Count{" + "moeny=" + moeny + '}'; } } ##########一共存10份钱,最后账户只剩下了800块钱。原因就是账户这个共享资源没有做好保护,导致账户钱少了。 user count:Count{moeny=100} user count:Count{moeny=800}
通过synchronized关键字进行共享资源的保护,从而安全的保证了多人存款。
public class SynchronizedExample { private Count wangxin = new Count(); public static void main(String[] args) { new SynchronizedExample().test(); } private void test() { System.out.println("user count:" + wangxin); List<Thread> threadList = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { threadList.add(new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } saveMoeny(); } })); } for (Thread each : threadList) { each.start(); } try { Thread.currentThread().join(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("user count:" + wangxin); } public synchronized void saveMoeny() { int moeny = wangxin.getMoeny(); wangxin.setMoeny(moeny + 100); } }
什么叫线程安全?
从上面第二个栗子应该可以看到线程安全的本质含义了。针对共享的资源,如果不做保护的话,同时被多个线程写操作会导致结果不可预期,可能每次跑的结果都不一致。所以线程安全就是在任何情况下同一段代码被多线程执行完毕,结果都一致,并且符合预期。
如何做到线程安全?
从上面的例子也可以看出有两种手段:
- 采用volatile关键字保证线程间状态变量的可见性
- 采用synchronized关键字保证线程对临界区的串行读写
volatile关键字在JVM中如何实现的?
JVM的内存模型中保证:
- volatile关键字修饰的变量都只从内存读取。不会从CPU的缓存中读取
- volatile关键字修饰的变量写操作,写回内存,同时CPU各级缓存该变量失效
由JVM上面两条就能保证,CPU的各个核都能从内存中读取到数据,从而保证了各个线程可见。
synchronized关键字在JVM中如何实现?
synchronized关键字要搞定的事情是临界区代码保证线程串行访问,就是上面的厕所坑串行的被使用。
Java编译器在.java文件被编译的时候,在临界区代码的入口处和出口处插入了monitorenter和monitorexit字节码指令。
同时在对象创建的时候,在其对象头部用两个字节(MarkWord)来表示该对象上是否有锁,同时被那条线程占用。