一、synchronized的功能扩展:重入锁
1.1. ReentrantLock类
重入锁可以完全代替synchronized关键字,使用java.util.concurrent.locks.ReentrantLock类来实现。
案例如下:
package multi_thread;
import java.util.concurrent.locks.ReentrantLock;
public class reenterlock01 implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10; j++) {
lock.lock(); // 进入重入锁临界区资源
try{
i++;
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException{
reenterlock01 rtl = new reenterlock01();
Thread t1 = new Thread(rtl);
Thread t2 = new Thread(rtl);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
重入锁是可以反复进入的,一个线程连续两次获得一把锁,这是允许的,但是你也得释放相同的次数,如下:
lock.lock();
lock.lock();
try{
i++;
}finally {
lock.unlock();
lock.unlock();
}
1.2. 中断相应
对于synchronized来说,如果一个线程在等待锁,那么结果如下:1.获得锁继续执行;2.保持等待
但是如果使用重入锁的话,那么还有一种可能: 线程可以中断。这样对于处理死锁是有帮助的。
使用方法:
public class reenterlock01 implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10; j++) {
try {
lock.lockInterruptibly(); // 锁的请求
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException{
reenterlock01 rtl = new reenterlock01();
Thread t1 = new Thread(rtl);
t1.start();
t1.sleep(100);
t1.interrupt(); // 中断操作,放弃对锁的申请,同时放弃以及获得的锁
System.out.println(i);
}
这时锁的请求使用 lockInterruptibly() 方法,这是一个可以对中断进行相应的锁申请动作,即在等待锁的过程中,可以响应中断。 上述这个例子不是死锁,只是简单介绍下操作。
1.3. 锁申请等待限时
方法: tryLock()
tryLock(5,TimeUnit.SECONDS) 5:等待时长 SECONDS:计时单位
如果超过5秒还没有得到锁,就会返回false。如果成功获得锁,则返货true。
如果不带参数,直接运行。此时如果锁被其他线程占领,则当前线程不会进行等待,而是直接返回false。
示例:
package multi_thread;
import java.util.concurrent.locks.ReentrantLock;
public class reenterLock2 implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public reenterLock2(int lock){
this.lock = lock;
}
@Override
public void run() {
if(lock==1){
while (true){
if(lock1.tryLock()){
try{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(lock2.tryLock()){
try{
System.out.println(Thread.currentThread().getId()+":My job done");
}finally {
lock2.unlock();
}
}
}finally {
lock1.unlock(); // 释放锁
}
}
}
}else{
while (true){
if(lock2.tryLock()){
try{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(lock1.tryLock()){
try{
System.out.println(Thread.currentThread().getId()+":My job done");
}finally {
lock1.unlock();
}
}
}finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
reenterLock2 r1 = new reenterLock2(1);
reenterLock2 r2 = new reenterLock2(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
上述代码中,采用了非常容易死锁的加锁顺序。先让t1获得lock1,再让t2获得lock2,接着做反向请求,让t1申请lock2,让t2申请lock1。一般情况下,它们会互相等待,引起死锁。
但是使用tryLock()后,线程是不断尝试的,只要执行足够的时间,线程总是会得到需要的资源的。
12:My job done
13:My job done
1.4. 公平锁
大多数情况下,系统只是从这个锁的等待队列中随机挑选一个等待线程,因此不能保证公平性,synchronized是非公平的。
公平的锁,会按照时间的先后顺序,保证先到者先得,后到者后得。
公平锁的一大特点:不会产生饥饿线现象。只要你排队,最终还是可以得到资源的。
重入锁允许我们对其公平性进行设置:
public ReentrantLock(boolean fair)
当 fair 为 true 时,表示锁是公平的。 公平锁的实现需要系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对低一点,没特殊要求,不需要使用。
1.5. ReentrantLock 的几个重要方法整理
二、重入锁的好搭档:condition条件先看下面代码, 通过lock 生成 condition对象。和Object.wait() 和 notify() 方法一样,当线程使用 Condition.wait()和signal()时,要求线程持有相关重入锁。
package multi_thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class reentrantLock_condition implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try{
lock.lock();
condition.await(); // 要求线程再condition对象上等待
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws Exception{
reentrantLock_condition r1 = new reentrantLock_condition();
Thread t1 = new Thread(r1);
t1.start();
Thread.sleep(100);
// 通知t1继续执行
lock.lock();
condition.signal(); // 告知再condition上等待的线程可以执行了
lock.unlock();
}
}
在Java内部,重入锁和Condition对象被广泛使用,以Array BlockingQueue为例:
以后补:
三、允许多个线程同时访问: 信号量(Semaphore)
信号量可以指定多个线程,同时访问某一个资源,信号量的构造函数如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
在构建信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了每次可以有多少个线程可以访问资源。
信号量的重要逻辑方法有:
public void acquire() // 尝试获得一个准入的许可,若无法获得,则线程会等待,直到有线程释放或者当前线程被中断
public void acquireUninterruptibly() // 不响应中断
public boolean tryAcquire() // 尝试获得一个许可
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release() // 访问资源结束后,释放一个许可
下面用一个例子来描述下semaphore的工作过程:
假设有10个工人,但是只有5台机器,每次只有5个工人在机器上工作。
public class worker_semaphore implements Runnable {
public int id;
public Semaphore semaphore;
public worker_semaphore(int id, Semaphore semaphore){
this.id = id;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("工人"+this.id+"正在机器上工作");
Thread.sleep(1000);
System.out.println("工人"+this.id+"释放机器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
int n = 10; //10个工人
Semaphore sema = new Semaphore(5);
for (int i = 1; i <=n ; i++) {
new Thread(new worker_semaphore(i,sema)).start();
}
}
}
输出:
工人1正在机器上工作
工人2正在机器上工作
工人3正在机器上工作
工人5正在机器上工作
工人6正在机器上工作
工人2释放机器
工人1释放机器
工人5释放机器
工人6释放机器
工人3释放机器
工人10正在机器上工作
工人9正在机器上工作
工人4正在机器上工作
工人7正在机器上工作
工人8正在机器上工作
工人4释放机器
工人8释放机器
工人10释放机器
工人9释放机器
工人7释放机器
四、ReadWriteLock 读写锁
(参考:http://blog.csdn.net/qq_19431333/article/details/70568478)
ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。
Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
ReentrantReadWriteLock可以用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类。下面是TreeMap使用ReentrantReadWriteLock进行封装成并发性能提高的一个例子:
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
五、CoutDownLatch 倒计时器
倒计时器,类似于火箭发射的场景,只有等所有的检查完毕后,引擎才能点火。这种场景适合使用CountDownLatch,它可以使得点火线程等待所有检查线程全部完工后,再执行。
CountDownLatch类只提供了一个构造器:
1
|
public
CountDownLatch(
int
count) { };
//参数count为计数值
|
然后下面这3个方法是CountDownLatch类中最重要的方法:
1
2
3
|
public
void
await()
throws
InterruptedException { };
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public
boolean
await(
long
timeout, TimeUnit unit)
throws
InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public
void
countDown() { };
//将count值减1,通知计时器
|
下面看一个例子大家就清楚CountDownLatch的用法了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public
class
Test {
public
static
void
main(String[] args) {
final
CountDownLatch latch =
new
CountDownLatch(
2
);
new
Thread(){
public
void
run() {
try
{
System.out.println(
"子线程"
+Thread.currentThread().getName()+
"正在执行"
);
Thread.sleep(
3000
);
System.out.println(
"子线程"
+Thread.currentThread().getName()+
"执行完毕"
);
latch.countDown();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new
Thread(){
public
void
run() {
try
{
System.out.println(
"子线程"
+Thread.currentThread().getName()+
"正在执行"
);
Thread.sleep(
3000
);
System.out.println(
"子线程"
+Thread.currentThread().getName()+
"执行完毕"
);
latch.countDown();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try
{
System.out.println(
"等待2个子线程执行完毕..."
);
latch.await();
System.out.println(
"2个子线程已经执行完毕"
);
System.out.println(
"继续执行主线程"
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
|
六、CyclicBarrier 循环栅栏
和CountDownLatch非常相似,它也可以实现线程间的技术等待,但它的功能更加强大。
前面的Cycli意为循环,也就是说这个计数器可以反复使用。比如,我们将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后截至凑齐下一批10个线程,这就是循环栅栏的内在含义。
CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:
1
2
3
4
5
|
public
CyclicBarrier(
int
parties, Runnable barrierAction) {
}
public
CyclicBarrier(
int
parties) {
}
|
参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
1
2
|
public
int
await()
throws
InterruptedException, BrokenBarrierException { };
public
int
await(
long
timeout, TimeUnit unit)
throws
InterruptedException,BrokenBarrierException,TimeoutException { };
|
第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
例子:
public class CyclicBarrierDemo {
public static class Solder implements Runnable{
private String solderName;
private final CyclicBarrier cyclic;
public Solder(String solderName,CyclicBarrier cyclic) {
this.solderName = solderName;
this.cyclic = cyclic;
}
@Override
public void run() {
try{
//等待所有士兵到齐,即等待栅栏条件满足
cyclic.await();
doWork();
cyclic.await(); //当一个士兵完成任务后,他就会要求CyclicBarrier开始下一次技术,这次计数主要目的是监控是否所有士兵都已经完成了任务
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public void doWork() throws InterruptedException {
Thread.sleep(100);
System.out.println(solderName+"任务完成");
}
}
public static class BarrierRun implements Runnable{
boolean flag;
int N;
public BarrierRun(boolean flag, int N){
this.flag = flag;
this.N = N;
}
@Override
public void run() {
if(flag){
System.out.println("士兵"+N+"个任务完成");
}else{
System.out.println("士兵"+N+"个集合完毕");
flag = true;
}
}
}
public static void main(String[] args) {
int N = 10;
Thread[] allSolder= new Thread[N];
boolean flag = false;
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag,N)); //创建了CyclicBarrier实例,并将计数器设置为10
//
System.out.println("集合队伍!");
for (int i = 0; i < N; i++) {
System.out.println("士兵"+i+"报道!");
allSolder[i] = new Thread(new Solder("solder"+i,cyclic));
allSolder[i].start();
}
}
}
输出:
集合队伍!
士兵0报道!
士兵1报道!
士兵2报道!
士兵3报道!
士兵4报道!
士兵5报道!
士兵6报道!
士兵7报道!
士兵8报道!
士兵9报道!
士兵10个集合完毕
solder2任务完成
solder3任务完成
solder9任务完成
solder1任务完成
solder4任务完成
solder0任务完成
solder6任务完成
solder8任务完成
solder7任务完成
solder5任务完成
士兵10个任务完成
上面这个例子,通过CyclicBarrier先监控是否所有的士兵都已经集合,然后士兵dowork(),然后再监控是否所有的士兵都完成任务。
CyclicBarrier.await()方法可能会抛出两个异常,一个是中断异常InterruptedException,一个是BrokenBarrierException,这个异常表明:一旦遇到这个异常,表示当前的CyclicBarrier已经破损了,可能系统已经没有办法等待所有线程到齐了。
六、线程阻塞类工具:LockSupport
6.1.Thread.suspend (线程挂起)和 resume(继续执行)
被挂起的线程,必须要等到resume()操作后,才能执行。但是现在这两个方法已经被标注为废弃方法,并不推荐使用。原因就在于使用suspend挂起线程的时候,并不会去释放任何锁资源,这就影响了其他任何想访问这些锁资源的线程。知道执行了resume()方法后,被挂起的线程才能继续,从而阻塞在相关锁上的线程才能继续。
但是,如果resume()操作在suspend()操作之前执行,那么被挂起的线程就很难被继续执行。而且, 它所占用的所并不会被释放,对于被挂起的线程,从线程状态上看,居然还是Runnable,严重影响了系统对当前状态的判断。
6.2.LockSupport 中的 park()、unpark()
与Thread.suspend()相比,它弥补了resume()执行在前的风险;与wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。
Demo:
public class lockSupportDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread{
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
synchronized(u){
System.out.println("in"+getName());
LockSupport.park(); //挂起
System.out.println(getName()+"unpark");
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
LockSupport.unpark(t1); //继续执行
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
Result:
int1
t1unpark
int2
t2unpark
Jps获取进程ID:
E:\Program Files\IntelliJ_workspace\Jz_offer_nowcoder>Jps
10720
12560 Launcher
12932
17028
2540 Jps
继续输入:jstack 10720(这是进程ID)
进行线程Dump,得到:
"History Command Saver" daemon prio=2 tid=0x000000011aff4000 nid=0x4654 waiting on condition [0x000000015802f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007eaa1b120> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source)
at java.util.concurrent.LinkedBlockingQueue.take(Unknown Source)
at com.mathworks.mde.cmdhist.AltHistoryCollection$CommandSaver.run(AltHistoryCollection.java:1206)
at java.lang.Thread.run(Unknown Source)
"RMI Scheduler(0)" daemon prio=6 tid=0x000000011affa000 nid=0x2104 waiting on condition [0x000000003476f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007ed4a7cf0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.getTask(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
可见park()挂起的线程会明确的给出一个WAITING状态,甚至还会标注是park()引起的。
原理: 因为LockSupport类使用类似信号量的机制。它为每一个线程准别了一个许可,如果许可可用,那么park()函数会立即返回,并且消费这个许可,如果许可不可用,就会阻塞。而unpark()使得一个许可可用,所以unpark()发生在park()之前,线程依然可以返回。
6.3.LockSupport.park()支持中断影响
有关中断:https://blog.csdn.net/canot/article/details/51087772
例子:
public class LockSupportIntDemo {
public static Object u = new Object();
static LockSupportIntDemo.ChangeObjectThread1 t1 = new LockSupportIntDemo.ChangeObjectThread1("t1");
static LockSupportIntDemo.ChangeObjectThread1 t2 = new LockSupportIntDemo.ChangeObjectThread1("t2");
public static class ChangeObjectThread1 extends Thread{
public ChangeObjectThread1(String name){
super.setName(name);
}
@Override
public void run() {
synchronized(u){
System.out.println("in "+getName());
LockSupport.park();
if(Thread.interrupted()){
System.out.println(getName()+"被中断了");
}
System.out.println(getName()+"执行结束"); //
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}
结果:
in t1
t1被中断了
t1执行结束
in t2
t2执行结束
可以看到,LockSuport.park()跟Thread.sleep(), Object.wait(), Thread.join()一样,是可以相应Interrupted中断的, 但是它不会抛出InterruptedException异常。
整理在这边了:https://blog.csdn.net/u012156116/article/details/79876017
下一篇:Java并发包学习(二):线程池 https://blog.csdn.net/u012156116/article/details/79439764