Java锁机制

Posted 沫小淘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java锁机制相关的知识,希望对你有一定的参考价值。

线程同步

什么是线程同步

线程之间执行是有先后顺序的,一个线程要等待上一个线程执行完之后才开始执行当前的线程。

为什么要线程同步

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占用的资源。

 

处理方法

预防死锁:破坏四个必要条件中的一个或多个

避免死锁

检测死锁

解除死锁

以上是关于Java锁机制的主要内容,如果未能解决你的问题,请参考以下文章

Java锁机制总结

java多线程——锁机制synchronized(同步方法)

java的锁机制——synchronized

#yyds干货盘点# Java | 关于synchronized相关理解

Java并发006使用层面:Lock锁机制全解析

java的锁机制