java多线程(线程同步)

一、同步方法

对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...

public synchronized void run() {
       
    // ....
 
}
View Code

二、同步代码块

正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

synchronized (obj) {

              //... 

}
View Code

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)

三、Lock对象同步锁

使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

class X{ 
  
    priavte final Lock lock = ReentrantLock();
    lock.lock();
    try{
        //处理任务
    }catch(Exception ex){
     
    }finally{
        lock.unlock();   //释放锁
    }

}
View Code

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

3.1ReentrantLock(重入锁)

重入锁可以完全替代synchronized关键字,在jdk5早期版本中重入锁的性能远远好于synchronized,但从JDK6开始JDK在synchronized中做了大量的优化,是的两者的性能差距不大。

ReentrantLock具有公平和非公平两种模式,也各有优缺点:
公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。

公平锁

大多数情况下锁的申请都是非公平的。如一个线程1先请求了锁A,然后线程2页也请求了锁A,那么当锁A可用时,是线程1可以获得锁还是线程2是不一定的,系统只是会从这个锁的等待队列中随机挑选一个。

重入锁允许我们对其公平性进行设置。公平锁的一大特点是:它不会产生饥饿现象。只要排队,最终你就可以获得资源。可以使用如下构造函数创建公平锁:

public ReentrantLock(boolean fair)
View Code

当参数fair为true,表示锁的公平的,当然由于公平所需要维护有序队列,因此公平锁的实现成本比较高,性能相对也底下,所以默认都是非公平锁。

public class Test {
    public static ReentrantLock lock = new ReentrantLock(true);
    public static void main(String[] args) throws Exception {
        Thread t1 = new TestThread();
        Thread t2 = new TestThread();
        t1.start();
        t2.start();
    }
}
class TestThread extends Thread {
    @Override
    public void run() {
        while(true)
        try {
            Test.lock.lock();
            System.out.println(Thread.currentThread().getName()+"获得锁");
        } finally{
            Test.lock.unlock();
        }
    }
}
View Code

可以看到如上代码制定公平锁之后,两个线程交替获得锁

Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
...
View Code

3.2ReentrantReadWriteLock(读写锁)

ReadWriteLock是JDK5开始提供的读写分离锁。读写分离开有效的帮助减少锁的竞争,以提升系统性能。用锁分离的机制避免多个读操作线程之间的等待。

读写锁的访问约束:

  • 读-读不互斥:读读之间不阻塞
  • 读-写互斥:读堵塞写,写也阻塞读
  • 写-写互斥:写写阻塞

如果在一个系统中读的操作次数远远大于写操作,那么读写锁就可以发挥明显的作用,提升系统性能

public class Test {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = reentrantReadWriteLock.readLock();
    private static Lock writeLock = reentrantReadWriteLock.writeLock();
    private static int value;

    public static Object handleRead(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);// 模拟读操作
            System.out.println("读操作:" + value);
            return value;
        } finally {
            lock.unlock();
        }
    }

    public static void handleWrite(Lock lock, int index)
            throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);// 模拟写操作
            System.out.println("写操作:" + value);
            value = index;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        TestReadThread testReadThread = new TestReadThread();
        TestWriteThread testWriteThread = new TestWriteThread();
        for (int i = 0; i < 18; i++) {
            new Thread(testReadThread).start();
        }
        for (int i = 18; i < 20; i++) {
            new Thread(testWriteThread).start();
        }

    }
    
    private static class TestReadThread extends Thread {
        @Override
        public void run() {
            try {
                //Test.handleRead(lock);
                Test.handleRead(readLock);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class TestWriteThread extends Thread {
        @Override
        public void run() {
            try {
                //Test.handleWrite(lock,new Random().nextInt(100));
                Test.handleWrite(writeLock,new Random().nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
View Code

参考https://www.cnblogs.com/whatadiors/p/8013086.html

四、lock与synchronized的区别

1、ReentrantLock (可重复锁)拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等。
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

ReentrantLock获取锁定与三种方式:

  • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
  • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
  • tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
  • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断


2、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

3、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

4、Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

五、原子操作CAS

5.0的多线程任务包对于同步的性能方面有了很大的改进,在原有synchronized关键字的基础上,又增加了ReentrantLock,以及各种Atomic类。了解其性能的优劣程度,有助与我们在特定的情形下做出正确的选择。

synchronized(同步关键字): 
在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。

ReentrantLock(可重复锁):
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

Atomic(原子类):
和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。

AtomicInteger用法

AtomicInteger提供原子操作来进行Integer的使用,因此十分适合高并发情况下的使用。

public class Sample2 {

    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.getAndIncrement();
    }

}
View Code

以上两段代码,在使用Integer的时候,必须加上synchronized保证不会出现并发线程同时访问的情况,而在AtomicInteger中却不用加上synchronized,在这里AtomicInteger是提供原子操作的,下面就对这进行相应的介绍。

CAS原子操作说明:一个旧值A,一个内存地址V,一个预期值B,把i值存到A当中,i++存到B当中,当且仅当V=A,才将B更新到V当中。

原子操作参考https://blog.csdn.net/u010904188/article/details/87712060

参考https://www.cnblogs.com/zhaoyan001/p/8885360.html

参考https://www.cnblogs.com/nsw2018/p/5821738.html

六、wait()/notify()/notifyAll()线程通信

wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

我们需要注意如下几点:

1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),

参考https://www.cnblogs.com/lwbqqyumidi/p/3821389.html

原文地址:https://www.cnblogs.com/yanmingyuan/p/10566207.html