Java并发包学习(一):坑终于填的差不多了

一、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异常。


七、Java多线程中的Exchanger

整理在这边了:https://blog.csdn.net/u012156116/article/details/79876017


下一篇:Java并发包学习(二):线程池 https://blog.csdn.net/u012156116/article/details/79439764

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值