java多线程

1、lock接口

方法签名描述
void lock();获取锁(不死不休)
boolean tryLock();获取锁(浅尝辄止)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;获取锁(过时不候)
void lockInterruptibly() throws InterruptedException;获取锁(任人摆布)
void unlock();释放锁
Condition newCondition();

结论:
1、lock()最常用;
2、lockInterruptibly()方法一般更昂贵,有的impl可能没有实现lockInterruptibly(),只有真的需要效应中断时,才使用,使用之前看看impl对该方法的描述。

2、有了synchronized为什么还要Lock?

synchronized的优缺点

优点:

  1. 使用简单,语义清晰,哪里需要点哪里。
  2. 由JVM提供,提供了多种优化方案(锁粗化、锁消除、偏向锁、轻量级锁)。
  3. 锁的释放由虚拟机来完成,不用人工干预,也降低了死锁的可能性。

缺点:无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等。

Lock的优缺点

优点:

  1. 所有synchronized的缺点 。
  2. 可以实现更多的功能,可以实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等,让synchronized缺点更多。

缺点:需手动释放锁unlock,新手使用不当可能造成死锁。

3、Condition

Object中的wait(),notify(),notifyAll()方法是和synchronized配合使用的,可以唤醒一个或者全部(单个等待集);Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/unpark机制);

协作方式死锁方式1(锁)死锁方式2(先唤醒,再挂起)备注
suspend/resume死锁死锁已弃用
wait/notify不死锁死锁只用于synchronized关键字
park/unpark死锁不死锁

实例:

package com.dongnao.concurrent.period6;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Demo3_Condition {
    private static Lock lock = new ReentrantLock();
    //condition来自哪个lock,signal方法的就要在相应的lock的lock()方法之后,唤醒的是被该lock锁住且被condition.await()阻塞的线程
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        Thread th = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                System.out.println("子线程或得锁...\n");
                try {
                    System.out.println("开始wait...\n");

                    condition.await();      // waiting  park
                    //await方法会释放lock锁,和waiting会释放synchronized类似
                    System.out.println("main unlock后,wait才结束...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });
        th.start();
        Thread.sleep(4000L);
        lock.lock();
        System.out.println("唤醒子线程...");
        //这个方法必须在lock.lock之后调用,否则会抛出异常,唤醒的是condition.signal()前面被lock.lock()中的lock锁锁住且被condition.await()阻塞的线程
        condition.signal();

        Thread.sleep(10000L);
        lock.unlock();
        System.out.println("main unlock...\n");
    }
}

子线程或得锁...

开始wait...

唤醒子线程...
main unlock...

main unlock后,wait才结束...

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
一个Condition包含一个等待队列,新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

4、ThreadLocal

ThreadLocal本质是个map,map的键就是每个线程对象,值就是在每个线程所设置的值。
常用方法:
initialValue()
get()
set()
remove():
ThreadLocal拥有的这个变量,在线程之间很独立的,相互之间没有联系。内存占用相对来说比较大。

package period1;

public class Demo5_ThreadLocal {
    /** threadLocal变量,每个线程都有一个副本,互不干扰 */
    public static ThreadLocal<String> value = new ThreadLocal<>();


    public void threadLocalTest() throws Exception {

        // threadlocal线程封闭示例
        value.set("123"); // 主线程设置值
        String v = value.get();
        System.out.println("主线程,v:" + v);

        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = value.get();
                System.out.println("子线程:线程1取到的值:" + v);

                // 设置 threadLocal
                value.set("456");

                v = value.get();
                System.out.println("子线程:重新设置之后,线程1取到的值:" + v);
                System.out.println("子线程 执行结束");
            }
        }).start();

        Thread.sleep(12000L); // 等待所有线程执行结束

        value.set("vvv"); // 主线程设置值
        v = value.get();
        System.out.println("主线程,v:" + v);

    }

    public static void main(String[] args) throws Exception {
        new Demo5_ThreadLocal().threadLocalTest();
    }
}

运行结果

主线程,v:123
子线程:线程1取到的值:null
子线程:重新设置之后,线程1取到的值:456
子线程 执行结束
主线程,v:vvv

5、Fork/Join框架

Fork/Join框架的作用

并行执行任务的框架,把大任务拆分成很多的小任务,汇总每个小任务的结果得到大任务的结果。

Fork/Join使用两个类来完成以上的事情。

1、ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
·RecursiveAction:用于没有返回结果的任务。
·RecursiveTask:用于有返回结果的任务。
2、ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。

执行流程梳理:
在这里插入图片描述
用例;

package com.dongnao.concurrent.period9;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class ForkJoinTest {

    static ArrayList<String> urls = new ArrayList<String>(){
        {
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
            add("http://www.baidu.com");
            add("http://www.sina.com");
        }
    };

    // 本质是一个线程池,默认的线程数量:CPU的核数
    static ForkJoinPool forkJoinPool = new ForkJoinPool(3,
            ForkJoinPool.defaultForkJoinWorkerThreadFactory,
            null,
            true);



    public static void main(String args[]) throws ExecutionException, InterruptedException {
        Job job = new Job(urls, 0, urls.size());
        ForkJoinTask<String> forkjoinTask =  forkJoinPool.submit(job);


       String result = forkjoinTask.get();
       System.out.println(result);
    }

    public static   String doRequest(String url, int index){
        //模拟网络请求
        return (index +  "Kody ... test ... " + url + "\n");
    }


    static class Job extends RecursiveTask<String>{

        List<String> urls;
        int start;
        int end;

        public Job(List<String> urls, int start, int end){
            this.urls = urls;
            this.start = start;
            this.end = end;
        }


        @Override
        protected String compute() {
            int count = end -start;         //计算这个任务有多大

            //什么时候对任务进行拆分
            if (count <=10){
                //直接执行
                String rsult = "";
                for (int i=start; i< end;i++){
                    String response = doRequest(urls.get(i), i);
                    rsult +=response;
                }
                return rsult;
            }else{
                //拆分任务
                int x = (start + end) / 2;

                Job job1 = new Job(urls, start, x);
                job1.fork();
                Job job2 = new Job(urls, x, end);
                job2.fork();

                //固定写法
                String result = "";
                result +=job1.join();
                result += job2.join();
                return result;
            }

        }
    }

}

6、并发工具类

a、CountDownLatch

允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。

调用countDownd方法计数器减一。

package com.dongnao.concurrent.period4_1;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;

//Count Down Latch 倒计时开关
//CountDownLatch的基本使用
public class Demo6_CountDownLatch {

    static AtomicLong num = new AtomicLong(0);

    public static void main(String args[]) throws InterruptedException {

        CountDownLatch ctl = new CountDownLatch(10);

        for (int i=0; i< 10; i++){
            new Thread(){
                @Override
                public void run() {
                    for (int j=0; j< 10000000; j++){
                        num.getAndIncrement();
                    }
                    ctl.countDown();//计数器减一
                }
            }.start();
        }

        ctl.await();     //设置开关,设置门栓,当计数器变为0时,开关打开
        System.out.println(num.get());
    }
}

b、Semaphore

Semaphore是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目。
简单说,是一种用来控制并发量的共享锁。

如下图,当有多个并发过来的时候,只有一定数量的线程能过通过运行,其他线程只能等待。
在这里插入图片描述
Semaphore构造函数指定通道数量,调用acquire方法获取一个信号量, 即抢占一个访问通道,调用release方法,释放信号量, 即通道释放。还可以用tryAcquire()方法尝试获取信号量。

package com.dongnao.concurrent.period4_1;

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.LockSupport;

public class Demo9_Semaphore {

    public static void main(String args[]){

        Semaphore sp = new Semaphore(10);//同一时间只能有10个通道

        //这么短的时间,发送1000个请求,并发会很高,数据库会受不了
        for (int i=0; i<1000; i++){
            new Thread(){
                @Override
                public void run() {
                    try {
                        sp.acquire();//获取一个信号量, 即抢占一个访问通道
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    queryDb("localhost:3306");  //模拟DB查询
                    sp.release();//释放信号量, 讲通道释放
                }
            }.start();
        }

    }
    //发送一个HTTP请求
    public static void queryDb(String uri)  {
        System.out.println("do query...:" + uri);
        LockSupport.parkNanos(1000 * 1000 * 1000);
    }
}

Semaphore还提供一些其他方法,具体如下:

  • intavailablePermits():返回此信号量中当前可用的许可证数。
  • intgetQueueLength():返回正在等待获取许可证的线程数。
  • booleanhasQueuedThreads():是否有线程正在等待获取许可证。
  • void reducePermits(int reduction):减少reduction个许可证,是个protected方法。
  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法。

c、CyclicBarrier

循环栅栏,可以循环利用的屏障。等待筹齐一定数量的线程之后再一起运行。
举例:排队上摩天轮时,每到齐四个人,就可以上同一个车厢。

package com.dongnao.concurrent.period4_1;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.LockSupport;

public class Demo10_CyclicBarrier {
    public static void main(String[] args) {

        CyclicBarrier barrier  = new CyclicBarrier(4,
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(">>> 这是一个栅栏。。。");
                    }
                });

        //传入一个Runnable,打印栅栏

        for (int i=0; i< 100; i++){
            new Thread(){
                @Override
                public void run() {
                    try {
                        //LockSupport.parkNanos(1000 * 1000 * 1000 * 10L);
                        barrier.await();    //
                        System.out.println("上到摩天轮...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
            LockSupport.parkNanos(1000 * 1000 * 1000L);
        }
    }

}

输出结果:

>>> 这是一个栅栏。。。
上到摩天轮...
上到摩天轮...
上到摩天轮...
上到摩天轮...
>>> 这是一个栅栏。。。
上到摩天轮...
上到摩天轮...
上到摩天轮...
上到摩天轮...
>>> 这是一个栅栏。。。
上到摩天轮...
上到摩天轮...
上到摩天轮...
上到摩天轮...
>>> 这是一个栅栏。。。
上到摩天轮...
上到摩天轮...
上到摩天轮...
上到摩天轮...
>>> 这是一个栅栏。。。
。。。。

d、Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

7、线程安全问题

死锁

一定发生在多个线程争夺多个资源里的情况下,发生的原因是每个线程拿到了某个(某些)资源不释放,同时等待着其他线程所持有的资源。解决死锁的原则就是确保正确的获取资源的顺序,或者获取资源时使用定时尝试机制。

常见的死锁:简单顺序死锁、动态顺序死锁。

预防简单顺序死锁的方法是保证正确的加锁顺序,不同线程抢占不同锁的顺序一致就不会造成死锁。

public class NormalTransfer implements ITransfer{
    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {
        synchronized (from){
            System.out.println(Thread.currentThread().getName()+" get "+from.getName());
            Thread.sleep(100);
            synchronized (to){
                System.out.println(Thread.currentThread().getName()
                        +" get "+to.getName());
                from.flyMoney(amount);
                to.addMoney(amount);
            }
        }
    }
}

但是由于上面代码中的from和to是调用者传进来的,所以不同线程传递的顺序是有可能是反的,那就是动态顺序死锁,这种情况可以有两种处理方式:
方式一:用哈希区分不同锁对象,另外为了防止哈希碰撞,另加入一个锁

public class SafeTransfer implements ITransfer {

    private static Object tieLock = new Object();

    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {

        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
//		用哈希值区分锁对象,以保证锁顺序
        if(fromHash<toHash){
            synchronized (from){
                System.out.println(Thread.currentThread().getName()+" get "+from.getName());
                Thread.sleep(100);
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                            +" get "+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        }else if(toHash<fromHash){
            synchronized (to){
                System.out.println(Thread.currentThread().getName()+" get "+to.getName());
                Thread.sleep(100);
                synchronized (from){
                    System.out.println(Thread.currentThread().getName()
                            +" get "+from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        }else{
            synchronized (tieLock){//当出现哈希碰撞时,用另一个锁来防止死锁,这里只能有一个线程进入
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()+" get "+from.getName());
                    Thread.sleep(100);
                    synchronized (from){
                        System.out.println(Thread.currentThread().getName()
                                +" get "+to.getName());
                        from.flyMoney(amount);
                        to.addMoney(amount);
                    }
                }
            }
        }
    }

方式二:循环动态抢锁

public class TryLockTransfer implements ITransfer {
    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {
        Random r = new Random();
        while(true){
            if(from.getLock().tryLock()){
                try{
                    System.out.println(Thread.currentThread().getName()
                            +" get from "+from.getName());

                    if(to.getLock().tryLock()){
                        try{
                            System.out.println(Thread.currentThread().getName()
                                    +" get to "+to.getName());
                            from.flyMoney(amount);
                            to.addMoney(amount);
                            System.out.println(from);
                            System.out.println(to);
                            break;
                        }finally {
                            to.getLock().unlock();
                        }
                    }

                }finally {
                   from.getLock().unlock();
                }
            }
            Thread.sleep(r.nextInt(5));//防止产生活锁
        }
    }
}

如何避免死锁

在这里插入图片描述

  1. 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。例如上面的死锁程序,主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
  2. 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
  3. 使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
  4. 死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。

对性能的思考

1、程序的安全性优于性能的提升
2、使用多线程会带来额外的性能开销,滥用线程,有可能导致得不偿失。
3、所谓性能,包含多个指标。例如“多快”:服务时间、等待时间、延迟时间;例如“多少”:吞吐量,例如可伸缩性等等。
4、性能的各个指标方面,是完全独立的,有时候甚至是相互矛盾。
5、所以性能的提升是个包括成本在内多方面权衡和妥协的结果。

性能优化的黄金原则:首先保证程序正确,然后再提高运行速度(如果有确切的证据表明程序确实慢)。

减少锁的竞争的方法

  1. 快进快出,缩小锁的范围,将与锁无关的,有大量计算或者阻塞操作的代码移出同步范围。
    在这里插入图片描述
  2. 减小锁的粒度,多个相互独立的状态变量可以使用多个锁来保护,每个锁只保护一个变量。
    在这里插入图片描述
  3. 减少独占锁的使用,例如读多写少的情况下,用读写锁替换排他锁。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值