线程同步
什么是线程同步
线程之间执行是有先后顺序的,一个线程要等待上一个线程执行完之后才开始执行当前的线程。
为什么要线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,所以需要线程同步执行,保证了该变量的唯一性和准确性。
如何实现线程同步
多线程的线程同步机制实际上是靠锁的概念来控制的。 1)synchronized关键字 2)java.util.concurrent.lock包中的Lock对象
synchronized关键字
synchronized 是Java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问。
1. 在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:1)保存在堆中的实例变量 2)保存在方法区中的类变量 这两类数据是被所有线程共享的(Java栈总的数据是线程私有的,不需要协调)。
2. 在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。)
3. 为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。 如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)
类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。
4. 一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。
5. 在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。
使用方式
1. synchronized关键字修饰方法。public synchronized void save(){} synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
2. synchronized关键字修饰代码块。synchronized(object){} 同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
案例1
public class ThreadTest extends Thread { private int threadNo; public ThreadTest(int threadNo) { this.threadNo = threadNo; } @Override public synchronized void run() { for(int i = 1; i < 10000; i++){ System.out.println("No." + threadNo + ":" + i); } } public static void main(String[] args) throws Exception { for(int i = 1; i < 10; i++){ new ThreadTest(i).start(); Thread.sleep(1); } } }
这个程序其实就是让10个线程在控制台上数数,从1数到9999。理想情况下,我们希望看到一个线程数完,然后才是另一个线程开始数数。但是这个程序的执行过程告诉我们,这些线程还是乱糟糟的在那里抢着报数,丝毫没有任何规矩可言。
但是细心的读者注意到:run方法还是加了一个synchronized关键字的,按道理说,这些线程应该可以一个接一个的执行这个run方法才对阿。
对于一个成员方法加synchronized关键字,这实际上是以这个成员方法所在的对象本身作为对象锁
在本例中就是 以ThreadTest类的一个具体对象,也就是该线程自身作为对象锁的。一共十个线程,每个线程持有自己 线程对象的那个对象锁。这必然不能产生同步的效果。
案例2
public class ThreadTest extends Thread { private int threadNo; private String lock; public ThreadTest(int threadNo, String lock) { this.threadNo = threadNo; this.lock = lock; } public void run() { synchronized(lock){ for(int i = 1; i < 10000; i++){ System.out.println("No." + threadNo + ":" + i); } } } public static void main(String[] args) throws Exception { String lock = new String("lock"); for(int i = 1; i < 10; i++){ new ThreadTest(i, lock).start(); Thread.sleep(1); } } }
该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest的构造函数,将这个对象赋值给每一个ThreadTest线程对象中的私有变量lock。
根据Java方法的传值特点,这些线程的lock变量实际上指向的是堆内存中的同一个区域,即存放main函数中的lock变量的区域。
程序将原来run方法前的synchronized关键字去掉,换用了run方法中的一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!
于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。
案例3
public class ThreadTest extends Thread { private int threadNo; public ThreadTest(int threadNo) { this.threadNo = threadNo; } public void run() { abc(threadNo); } public static void main(String[] args) throws Exception { for(int i = 1; i < 20; i++){ new ThreadTest(i).start(); Thread.sleep(1); } } public static synchronized void abc(int threadNo) { for(int i = 1; i < 10000; i++){ System.out.println("No." + threadNo + ":" + i); } } }
这段代码没有使用main方法中创建的String对象作为这10个线程的线程锁。而是通过在run方法中调用本线程中一个静态的同步 方法abc而实现了线程的同步。
这里synchronized静态方法是用什么来做对象锁的呢?对于同步静态方法,对象锁就是该静态放发所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例就是唯一的 ThreadTest.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!
总结
1. 对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;
2. 如果采用method级别的同步,则对象锁即为method所在的对象,如果是静态方法,对象锁即指method所在的Class对象(唯一);
3. 对于代码块,对象锁即指synchronized(abc)中的abc;
4. 同步有两种方式,同步块和同步方法。
如果是同步代码块,则对象锁需要编程人员自己指定,一般有些代码为synchronized(this)只有在单态模式才生效。如果是同步方法,则分静态和非静态两种,静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法(不用担心是否单例)。
5. 在Java多线程编程中,最常见的synchronized关键字实际上是依靠对象锁的机制来实现线程同步的。
Lock详解http://www.cnblogs.com/aishangJava/p/6555291.html
死锁问题
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态 或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的条件
互斥条件
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
请求和保持条件
指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
不剥夺条件
进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
环路等待条件
指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
处理方法
预防死锁:破坏四个必要条件中的一个或多个
避免死锁
检测死锁
解除死锁