Java:多线程的同步方式和锁

Posted 一个理想主义者的奋斗

tags:

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

Object.wait(miliSec)/notify()/notifyAll()

线程调用wait()之后可以由notify()唤醒,如果指定了miliSec的话也可超时后自动唤醒。wait方法的调用会让当前线程放弃已经获取的object锁标志位,比如在同步代码块synchronized中调用wait(),则表示当前线程被唤醒之后需要重新获取同步代码块的锁。另外wait/notify由于要操作对象的锁标志位,因此必须在synchronized代码块中调用,否则会抛出运行时异常IllegalMonitorStateException。


wait/notify机制出现之前,生产/消费实现模型的同步一般通过while(true)轮询实现,弊端是极大耗用CPU资源做无用的轮询。在调用wait方法之前,线程需要获取当前实例对象的锁,执行wait方法返回之后,线程释放掉对象锁并进入block状态;其他线程在调用notify方法之前,也需要获取当前实例对象的锁,执行notify方法时,如果有多个线程处理block状态则从中按某规则选择一个唤醒,notify方法调用之后不会立即释放锁,要等线程的同步方法执行完毕之后才释放对象锁,因此一次notify调用只会唤醒一个线程,其他block的线程依旧处理block状态。

 1 public class App1 extends Thread {
 2     private Object lock;
 3     public App1(Object lock) {
 4         super();
 5         this.lock = lock;
 6     }
 7     @Override
 8     public void run() {
 9         try {
10             synchronized (lock) {
11                 System.out.println(Thread.currentThread().getName()
12                                   + " : start to wait.");
13                 lock.wait();
14                 System.out.println(Thread.currentThread().getName() 
15                                                + " : wait ends, execute again.");
16             }
17         } catch (Exception e) {}
18     }
19 }
20 public class App2 extends Thread {
21     private Object lock;
22     public App2(Object lock) {
23         super();
24         this.lock = lock;
25     }
26     @Override
27     public void run() {
28         synchronized (lock) {
29             System.out.println(Thread.currentThread().getName() 
30                                     + " : Start notify.");
31             lock.notify();
32             System.out.println(Thread.currentThread().getName() 
33                                     + " : notify ends, start to execute again.");
34         }
35     }
36     public static void main(String[] args) {
37         try {
38             Object lock = new Object();
39             App1 app1 = new App1(lock);
40             app1.start();
41             Thread.sleep(5000);
42             App2 app2 = new App2(lock);
43             app2.start();
44         } catch (Exception e) {}
45     }
46 }
47          

 

Thread.sleep(miliSec)

线程释放CPU使用权,并进入休眠状态一段时间miliSec,不会放弃线程的锁标志位,比如如果在同步代码块synchronized中调用sleep(),表示线程将一直持有当前同步代码块的锁,其他线程将一直等待。

 

Thread.suspend()/resume()

两个方法配套使用,suspend进入的状态必须有resume调用恢复。跟sleep()方法类似,suspend方法也不会放弃线程已经获取的object锁标志位。这对方法已经不推荐使用,因为容易造成线程自己将自己suspend起来。


Thread.yield()

表示当前线程已经获得了充分的CPU执行时间,释放CPU使用权,并重新进入队列等待执行,yield()调用不会阻塞当前线程,也不会放弃当前线程已经获取的锁标志位 ,因此yield方法仅能让跟当前线程具有同样优先级的线程有限执行。


Thread.join(miliSec)

表示当前线程需要等到join方法的调用线程执行完毕之后才能继续执行,或者是等待join方法的调用线程执行一段时间之后当前线程才能执行,内部由wait方法实现,所以线程等待开始的时候就会释放持有的对象锁。


使用synchronized关键字修饰代码块或者方法

表示这块代码为互斥区或者临界区。有两种类型的锁可以通过synchronized加到代码块或者方法上,一种是实例Object锁,一种是class锁。对于同一个ClassLoader下加载的类而言,一个类只有一把class锁,所有这个类的实例都共享一把锁;同一个类可以实现多个实例对象,也就存在多把Object锁。
synchronized是语言自带的内置独享锁(非公平锁,不管race thread排队的时间先后,通过编排字节码实现,锁为对象或者类的头标记位),而Java语言的ReentraintReadWriteLock机制是基于Abstract Queued Synchronizer的一种实现(公平/非公平锁,state加CLH队列实现),主要的实现类是ReentrantLock;Java的数据主要会在CPU、Register、Cache、Heap和Thread stack之间进行复制操作,而前面四个都是在Java Threads之间共享,因此Java的锁机制主要用于解决Racing Threads的数据一致性;


另外通过synchronized添加的锁具有可重入性,也就是只要一个线程已经获取了锁,这样只要共享同一把锁的其他synchronized修饰的代码块或者方法都可以进入,换句话说其他线程访问对其他synchronized修饰的代码块或者方法也需要等待锁的释放,因此synchronized还支持任意对象的锁,这样同一个类的不同方法可以添加不同的对象锁。

 1 public class App1 {
 2     private Object lock1 = new Object();
 3     private static Object lock2 = new Object();
 4     
 5     synchronized public void funcA() {
 6         //this object lock
 7     }
 8     public void funcB() {
 9         synchronized(this) {
10             //this object lock
11         }
12         //run something without lock
13     }
14     public void funcC(List<String> list) {
15         synchronized(list) {
16             //list object lock
17         }
18     }
19     public void funcD() {
20         synchronized(lock1) {
21             //lock1 object lock
22         }
23     }
24     public void funcE() {
25         synchronized(lock2) {
26             //lock2 static object lock
27         }
28     }
29     public void funcF() {
30         synchronized(App1.class) {
31             //App1 class lock
32         }
33     }
34     synchronized public static void funcG() {
35         //App1 class lock
36     }
37 }

 

使用ReentraintLock和ReentraintReadWriteLock实现线程的同步

java的lock机制基于Abstract Queued Synchronizer (AQS)的实现,AQS定义了多线程访问共享资源的同步器框架,常见的如ReentraintLock/Semaphore/CountDownLatch等都依赖于AQS的实现;

AQS通过维护一个FIFO队列,并且通过一个由volatile修饰的int状态值来实现锁的获取。FIFO队列中每一个Node表示一个排队线程,其保存着线程的引用和状态,然后通过三个方法分别对获取或者设置状态。

 1 private volatile int state;
 2 static final class Node {
 3     int waitStatus;
 4     Node prev;
 5     Node next;
 6     Node nextWaiter;
 7     Thread thread;
 8 }
 9 protected final int getState() {
10         return state;
11     }
12 protected final void setState(int newState) {
13         state = newState;
14     
15 protected final boolean compareAndSetState(int expect, int update) {
16         // See below for intrinsics setup to support this
17         return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
18 }
19 protected boolean tryAcquire(int arg) 
20 protected boolean tryRelease(int arg)
21 protected int tryAcquireShared(int arg)
22 protected boolean tryReleaseShared(int arg) 
23 protected boolean isHeldExclusively() 

 

通过对getState,setState和compareAndSetState的封装,AQS的继承类需要试下如下几个方法,前面两个表示获取和释放独占锁(如ReentraintLock),后面两个表示获取和释放共享锁(如Semaphore和CountDownLatch)。ReentrantLock初始化状态state=0,线程A访问同步代码的时候使用ReentrantLock.lock(),内部会调用tryAcquire尝试获取独占锁,状态变成state+1;其他线程调用ReentrantLock.lock()的时候就会失败,直到线程A调用unlock(内部为tryRelease)将状态编程state=0;如果线程A在持有独占锁的同时访问其他同步代码块,这时候state的值就会累加,需要调用unlock(内部为tryRelease)减少state的值。ReentrantLock也提供了类似wait/notify的方法,await/signal,同样的线程在调用这两个方法之前需要获得对象锁监视,也就是执行lock.lock()方法。


ReentrantLock是纯粹的独占锁,为了提升效率引入了ReentrantReadWriteLock.readLock/writeLock,读读共享,读写互斥,写写互斥。CLH队列中的节点模式分为shared和exclusive两种,当一个线程修改了state状态则表示成功获取了锁,如果线程的模式是shared则会执行一个传递读锁的过程,策略是从CLH队列的头到尾依次传递读锁,直到遇到一个模式为exclusive的写锁模式的节点,这个exclusive模式的节点需要等之前所有shared模式的节点对应的操作都执行完毕之后才会获取到锁,这就是读写锁的模式。

 1 public class App1 extends Thread {
 2     private Lock lock = new ReentrantLock();
 3     private Condition condition = lock.newCondition();
 4     public App1() {
 5         super();
 6     }
 7     @Override
 8     public void run() {
 9         try {
10             lock.lock();
11             System.out.println(Thread.currentThread().getName() + " : start to wait.");
12             condition.await();//condition.signal();
13             System.out.println(Thread.currentThread().getName()
14                    + " : wait ends, execute again.");
15         } catch (Exception e) {
16             
17         } finally {
18             lock.unlock();
19         }
20     }
21 }

 










以上是关于Java:多线程的同步方式和锁的主要内容,如果未能解决你的问题,请参考以下文章

java并发之线程同步(synchronized和锁机制)

第七天学习多线程同步和锁

第七天学习多线程同步和锁

Java虚拟机--线程安全和锁优化

多线程 Thread 线程同步 synchronized

多线程基础