多线程的锁相关内容

重入锁

1.重入锁基本操作:

public class ReentranLockTest implements Runnable{
    private static ReentrantLock lock = new ReentrantLock();
    
    public void run() {
            // TODO Auto-generated method stub
            lock.lock();
            System.out.println("i get the lock");
            System.out.println("other threads can not reach this area");
            lock.unlock();
            }
}

如果一个线程在lock.lock()到lock.unlock()之间,其他线程如果也在访问这段,将会阻塞在lock.lock()上,即在等待获取锁。

重入锁叫重是因为获取锁的那个线程可以设置多重锁,如果需要,可以:

    @Override
    public void run() {
        // TODO Auto-generated method stub
        
        lock.lock();
        lock.lock();
        System.out.println("i get the lock");
        lock.unlock();
        lock.unlock();

需要记得的是,锁定了几次就要解锁几次,否则就其他线程就永远无法获得锁了。

2.可中断的重入锁

首先重入锁的使用场景是,如果线程A已经占据着锁了,另外一个线程B在申请锁的过程中必然阻塞,如果这个时候线程B设置了中断变量,如果是用Synchronsized或者1中的lock.lock()来阻塞等待的话,是无法相应中断的。所以引入了 lock.lockInterruptibly();

示例代码:

public class ReentranLockTest implements Runnable{
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        // TODO Auto-generated method stub//        
        try {
            //由于锁被主线程持有,所以此时阻塞在这里。
            lock.lockInterruptibly();
            System.out.println("get the lock");
            lock.unlock();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            System.out.println("give up the lock");
            e.printStackTrace();
        }   
        
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //主线程先获取锁
        lock.lock();
        Thread t = new Thread(new ReentranLockTest());
        t.start();
        //将线程中断标志位置为中断
        t.interrupt();
        lock.unlock();
    }
}

控制台打印如下:

give up the lock
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at cn.senninha.concurrent.annotations.ReentranLockTest.run(ReentranLockTest.java:15)
    at java.lang.Thread.run(Thread.java:745)

线程在等待获取锁的情况下依然响应了中断,放弃对锁的申请。

3.tryLock()

    public class ReentranLockTest implements Runnable {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    @Override
    public void run() {
        // TODO Auto-generated method stub//
        try {
            //等待1s,如果1s内不能获得锁,返回false。
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                System.out.println("get the lock");
                lock.unlock();
            } else {
                System.out.println("cannot get the lock after 1s");
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        lock.lock();
        Thread t = new Thread(new ReentranLockTest(), "b");
        t.start();

    }
}

运行结果:

cannot get the lock after 1s

4.重入锁配合Conditon使用

ReentrantLock与Condition的使用与 Synchronized与wait的使用搭配有相似之处。

首先Condition由ReentrantLock.newCondition()来实例化,实际上实例化是这样:

  final ConditionObject newCondition() {
            return new ConditionObject();
        }

public class ReentranLockTest implements Runnable{
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    @Override
    public void run() {
        // TODO Auto-generated method stub//        
        try {
            lock.lockInterruptibly();
            System.out.println("wait");
            System.out.println(System.currentTimeMillis());
            //等待。需要唤醒才能停止等待。
            condition.await();
            System.out.println("after wait");
            System.out.println(System.currentTimeMillis());
            lock.unlock();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            System.out.println("give up the lock");
            e.printStackTrace();
        }   
        
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //首先启动线程,线程马上进入等待状态,并释放锁
        Thread t = new Thread(new ReentranLockTest());
        t.start();
        //然后主线程重新申请锁
        lock.lock();
        try {
        //等待1s后唤醒其他等待的线程。
            condition.await(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        condition.signal();
        //记得释放当前的锁
        lock.unlock();
    }
}

运行结果:

wait
1491476750599
after wait
1491476751599

如预期,线程b先获取锁,然后在调用condition.await()后释放锁,然后主线程获取了锁,并且等待了1s后唤醒在condition上等待的线程b,然后主线程释放锁,等待的b线程重新申请获取锁后继续运行。

需要注意的点:

  • 调用conditon的相关方法需要在获取锁的前提下,否则会抛出

java.lang.IllegalMonitorStateException

  • 在等待过程中会释放锁,这个和Object.wait()是一样的。

5.信号量(Semaphore)

信号量可以限制临界区可以有多少个线程进入:

public class SemaphoreDemo implements Runnable{
    //第一个参数是信号量个数,第二个是是否是公平的信号量。
    private static Semaphore semaphore = new Semaphore(5,true);

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            //申请信号量
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
            //休眠1s
            Thread.sleep(1000);
            //释放信号量
            semaphore.release();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args){
        SemaphoreDemo demo = new SemaphoreDemo();
        //申请10个线程
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for(int i = 0 ; i < 10 ; i++){
            exec.submit(demo);
        }
        try {
            /休眠3s后停止线程池
            Thread.sleep(3000);
            exec.shutdown();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

由于只允许5个线程同时进入临界区,所以十个线程会分两批进入,那么打印出的时间戳应该是差1000ms的,结果如下:

pool-1-thread-11491479241712
pool-1-thread-31491479241715
pool-1-thread-21491479241715
pool-1-thread-51491479241716
pool-1-thread-41491479241716

pool-1-thread-61491479242713
pool-1-thread-71491479242715
pool-1-thread-81491479242715
pool-1-thread-91491479242717
pool-1-thread-101491479242717

可以看到先打印出来的前5个和后5个差了1000ms

4.读写锁(ReadWriteLock)

读写锁就是可以多个读进程进入,在读多写少的应用场景,可以大大提高效率。

当前操作新增加操作新增操作是否阻塞
阻塞
非阻塞
阻塞
阻塞

来个先写然后读的操作示例:


public class ReadWriteLockDemo {
    //实例化读写锁
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static ReadLock rLock = lock.readLock();
    private static WriteLock wLock = lock.writeLock();

    private static void read(Lock rLock) {
        //在申请读取锁不等待1s
        rLock.lock();
        System.out.println(Thread.currentThread() + "read get the lock");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread() + "read release the lock");
            rLock.unlock();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private static void wirte(Lock wLock) {
        //在申请写锁后等待1s
        wLock.lock();
        System.out.println(Thread.currentThread() + "write get the lock");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread() + "write release the lock");
            wLock.unlock();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        long start = System.currentTimeMillis();
        
        //读线程
        Thread t0 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                read(rLock);
            }

        }, "t0");

        //写线程
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                wirte(wLock);
            }

        }, "t1");

        try {
            //为了确保读操作先进行
            t0.start();
            //,主线程休眠100ms
            Thread.sleep(100);
            //写线程开始
            t1.start();
            //主线程等待t1线程完成
            t1.join();
            //获取消耗时间
            System.out.println("cost time" + (System.currentTimeMillis() - start));
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

消耗时间2s,因为读操作的时候,会阻塞写操作。

Thread[t0,5,main]read get the lock
Thread[t0,5,main]read release the lock
Thread[t1,5,main]write get the lock
Thread[t1,5,main]write release the lock
cost time2003

多线程读取,将代码做一点小修改

Thread t0 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                read(rLock);
            }

        }, "t0");

        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                wirte(wLock);
            }

        }, "t1");

结果1103ms,因为我们在线程之间等待了100ms,所以1103ms减去100ms就是1s了,说明多线程的读是不阻塞的。

Thread[t1,5,main]read get the lock
Thread[t0,5,main]read release the lock
Thread[t1,5,main]read release the lock
cost time1103

5.CountDownLatch(倒计时器)

倒计时器可以设置进入临界区的线程数量到了某个数量后就进停止等待,往下执行

首先是临界区设置,每进入一个线程计数一次:

CountDownLatch.countDown();

然后一个线程等待,只有指定数量的线程进入后才会停止等待

CountDownLatch.await();

具体代码:

public class ConutDownLatchTest implements Runnable{
    //指定五个线程计数量
    private static CountDownLatch cdl = new CountDownLatch(5);
    
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            Thread.currentThread().sleep(1000);
            //计数
            cdl.countDown();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("count");
    }

    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService service = Executors.newFixedThreadPool(5);
        ConutDownLatchTest test = new ConutDownLatchTest();
        for(int i = 0 ; i < 5 ; i++){
            service.submit(test);
        }
        try {
            //等待5个计数的线程进入
            cdl.await();
            System.out.println("five thread finish");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

其实就是五次调用countDown()计数后即可使之停止等待。。。循环里改成五次调用countDown()函数,然后就会发现不会等待。。直接就继续运行了。。

6.CyclicBarrier(循环栅栏)

循环栅栏,是要等到所有的线程都准备完毕了才会继续执行

public class CyclicBarrierTest implements Runnable{
    private static CyclicBarrier barrier = new CyclicBarrier(5);

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            //开始等待五个线程进入啦
            barrier.await();
            //如果是等待五个线程才继续执行,那么这里五个线程打印的时间应该是很接近的,后面看运行结果
            System.out.println("run~" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService service = Executors.newFixedThreadPool(5);
        for(int i = 0 ; i < 5 ; i++){
            try {
                //提交线程进入,为了显示是五个线程准备好才进入的,休眠1000ms
                service.execute(new CyclicBarrierTest());
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    

}

运行结果:

run 1491582362091
run 1491582362092
run 1491582362092
run 1491582362092
run 1491582362092
果然是等待五个线程后才一起执行。

7.LockSupport 线程阻塞工具

与wait()相比,LockSuport不用获取某个对象的锁,并且不会抛出中断异常InterruptedExcetion
示例代码如下,

public class LockSupportTest implements Runnable{
    
    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName() + "before park");
        //挂起当前线程
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "after park");
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        LockSupportTest lt0 = new LockSupportTest();
        LockSupportTest lt1 = new LockSupportTest();
        Thread t1 = new Thread(lt0,"t1");
        Thread t2 = new Thread(lt1,"t2");
        t1.start();
        t2.start();
        //使线程2停止挂起
        LockSupport.unpark(t2);
    }




}

运行结果:

t1before park
t2before park
t2after park

而且LockSuppor.unpark()方法发生在LockSupprot.park()之前也不会影响
比如,稍微修改一下run方法:

@Override
    public void run() {
        // TODO Auto-generated method stub
        //挂起之前就unpark()
        LockSupport.unpark(Thread.currentThread());
        System.out.println(Thread.currentThread().getName() + "before park");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "after park");
    }

结果:

t1before park
t1after park
t2before park
t2after park

结果当然是可以正常结束,有点像信号量的意思。。但是这个这个unpark()是不像信号量一样可以累加的。有且只有一个。



作者:senninha
链接:https://www.jianshu.com/p/4d054a417de5
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2018-05-12 15:16  邱明成  阅读(215)  评论(0编辑  收藏  举报