浅析Java锁机制


线程安全问题

虽然多线程编程极大地提高了效率,但是也会带来一定的隐患。举一个例子:我们要两个线程修改并交替打印变量a

public class VolatileDemo {
    int a = 0;

    public void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意这里
    }

    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    volatileDemo.addNum();
                    System.out.println("num=" + volatileDemo.a);
                }

            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    volatileDemo.addNum();
                    System.out.println("num=" + volatileDemo.a);
                }
            }
        }).start();
    }
}

还是这个例子,两个线程同时操作一个变量a,但是打印出来的a的最终结果一定会是200吗?答案不是。可能每次的结果都不一样。最终结果小于等于200。这就是最经典的一个线程安全问题了。两个线程操作一个变量可能两个线程同事拿到了变量的副本假设a=1,线程一,和线程二同时修改了变量副本两个线程中a=2了,这时候线程1和线程二再次将变量刷新到主内存中,等于说两个操作打印出同一个数字了,按照我们的意愿,这时候主内存中变量a应该等于3,但是还是等于2。

这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。。

  • 由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。

如何解决线程安全问题?

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

synchronized

  • synchronized用于多线程设计,有了synchronized关键字,多线程程序的运行结果将变得可以控制。synchronized关键字用于保护共享数据。
  • synchronized实现同步的机制:synchronized依靠”锁”机制进行多线程同步,”锁”有2种,一种是对象锁,一种是类锁。

互斥锁

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

synchronized的使用

synchronized可以修饰一个方法

在这个时候,synchronized获取的是该类的对象锁。等于说我同一个对象中的两个方法被synchronized修饰,那么这两个方法都是互斥的。第一个线程再访问的第一个方法的时候,第二个线程也必须要等待第一个线程完成了,才能访问第二个方法。

 public synchronized void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意这里
    }

那么上面的代码加上一个synchronized。运行结果就一定了,最终打印出来的结果是200了。

举例:

public class TestSynchronized {
    public synchronized void method1() throws InterruptedException {
        System.out.println("method1 begin at:" + System.currentTimeMillis());
        Thread.sleep(6000);
        System.out.println("method1 end at:" + System.currentTimeMillis());
    }
    public synchronized void method2() throws InterruptedException {
        while(true) {
            System.out.println("method2 running");
            Thread.sleep(200);
        }
    }
    static TestSychronized instance = new TestSychronized();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance.method1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=1; i<4; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1 still alive");
                }                    
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();    

    }
}

运行结果:thread2一直等到thread1中的method1执行完了之后才执行method2,说明method1和method2互斥

synchronized {修饰代码块}的作用不仅于此,synchronized void method{}整个函数加上synchronized块,效率并不好。在函数内部,可能我们需要同步的只是小部分共享数据,其他数据,可以自由访问,这时候我们可以用 synchronized(表达式){//语句}更加精确的控制。

synchronized可以修饰代码块

当修饰代码块的时候锁的就是传入的对象,this只的是当前类。

如下:

public  void addNum() {
    synchronizedthis{
           try {
              Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
             }
            a++;
        }
   }
synchronized修饰静态方法

synchronized还可以修饰静态方法,这个时候锁的对象就是类对象,下面两种例子效果都相同:

public void addNum(){
   synchronized(Obl.class)
   }
}
 public static synchronized void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意这里
    }

例子:

public class TestSychronized {
    public synchronized static void method1() throws InterruptedException {
        System.out.println("method1 begin at:" + System.currentTimeMillis());
        Thread.sleep(6000);
        System.out.println("method1 end at:" + System.currentTimeMillis());
    }
    public synchronized static void method2() throws InterruptedException {
        while(true) {
            System.out.println("method2 running");
            Thread.sleep(200);
        }
    }
    static TestSychronized instance1 = new TestSychronized();
    static TestSychronized instance2 = new TestSychronized();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance1.method1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=1; i<4; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1 still alive");
                }                    
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance2.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();    

    }
}

运行结果:thread2一直等到thread1中的method1执行完了之后才执行method2,说明method1和method2互斥

总结

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。synchronized既能保证原子性,又能保证一致性。synchronized锁的同一个对象的时候,其他线程不能访问该对象中的其他的synchronized修饰的方法或者代码块,他们是互斥的。虽然synchronized可以保证线程的同步,但是在访问线程非常多的情况下,性能低下。

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

Lock

lock是一个接口,它有如下方法:

public interface Lock {
    void lock();//加锁
    void lockInterruptibly() throws InterruptedException;//加可中断锁
    boolean tryLock();//加锁成功返回true,失败返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加锁成功返回true,失败等待一段时间后若仍无法加锁则返回false,可响应中断
    void unlock();//解锁
    Condition newCondition(); //返回一个Condition,利用它可以如和Synchronizd配合使用的wait()和notify()一样对线程阻塞和唤醒,不同的是一个lock可以有多个condition.
}

Lock方法

lock()

用来获取锁。如果锁已被其他线程获取,则等待。

Lock lock = ...;
if(lock.tryLock()) {
 try{
     //处理任务
 }catch(Exception ex){

 }finally{
     lock.unlock();   //释放锁
 } 
}else {
//如果不能获取锁,则直接做其他事情
}
tryLock()

用来获取锁。如果锁已被其他线程获取,则返回false,否则返回true。不会进行等待。

Lock lock = ...;
if(lock.tryLock()) {
 try{
     //处理任务
 }catch(Exception ex){

 }finally{
     lock.unlock();   //释放锁
 } 
}else {
//如果不能获取锁,则直接做其他事情
}
tryLock(long time, TimeUnit unit)

与tryLock()方法类似,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()

当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

public void method() throws InterruptedException {
        lock.lockInterruptibly();
        try {  
             //.....
        }
        finally {
         lock.unlock();
        }  
   }

ReentrantLock(Lock接口实现类)

是一个独占锁,与sychronized类似

ReadWriteLock

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

ReentrantReadWriteLock(ReadWriteLock实现类)

  • ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
  • ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是FairSync或者NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样。
  • ReentrantReadWriteLock里面有两个类:ReadLock/WriteLock,这两个类都是Lock的实现。
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。但是如果其他线程要申请读锁,那么不需要等待依然能申请的到读锁。这很大的优化了性能。
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

小结

ReentrantReadWriteLock相比ReentrantLock的最大区别是:ReentrantReadWriteLock的读锁是共享锁,任何线程都可以获取,而写锁是独占锁。ReentrantLock不论读写,是独占锁。

Lock和synchronized的选择

Lock和synchronized有以下几点不同:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5. Lock可以提高多个线程进行读操作的效率。

    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

锁的概念相关介绍

1.可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁

可重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

2.可中断锁

可中断锁:顾名思义,就是可以interrupt()中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

3.公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

4.读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突,提高了程序的性能。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。


文章作者: jackey
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jackey !
评论
  目录