多线程二,线程同步Synchronized 和 ReentrantLock用法,死锁,线程池4种常用方式

导读:本篇文章讲解 多线程二,线程同步Synchronized 和 ReentrantLock用法,死锁,线程池4种常用方式,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1 线程同步Synchronized 和 ReentrantLock用法

同步异步
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就 是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
相似点
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行 线程阻塞和唤醒的代价是比较高的.

synchronized
修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经 拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释
放为止。

public synchronized void mehthodA(){
    //业务代码。。。。
}
public synchronized static void mehthodB(){
}
@Override
public void run() {
    while(ticketNum>0){
        synchronized (this){
            if(ticketNum>0){
                System.out.println(Thread.currentThread().getName()+"正在卖票,剩余"+(--ticketNum)+"张");
            }
        }
    }
}

ReentrantLock
而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配
合try/finally语句块来完成。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,
ReentrantLock类提供了一些高级功能,
主要有以下3项:

1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized来说可以避免出现死锁的情况。

2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性
能不是很好。

3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。

while(ticketNum>0){
    try {
        //加锁
        reentrantLock.lock();
        if(ticketNum>0){
            System.out.println(Thread.currentThread().getName()+"正在卖票,剩余"+(--ticketNum)+"张");
        }
    } finally {
        //解锁
        reentrantLock.unlock();
    }
}

2 sleep和wait的区别

     sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线 程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一 个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法 (notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果 notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果),调用wait方法的 线程就会解除wait状态和程序可以再次得到锁后继续向下运行。
  package com.aaa.mt.demo2;
/**
 * @ fileName:SleepAndWaitDemo
 * @ description: sleep和wait区别:
 *         1,sleep 在不在同步块中都可以执行(不在同步块直接让出cpu,在同步块回不会释放锁)   wait 必须在同步块执行
 *         2,sleep 是Thread类中的静态方法    wait是Object类的方法
 *         3,都在同步块中时,sleep不会释放锁   wait会释放锁产生阻塞直到有其他拿锁唤醒才具备拿锁资格,并不是立马拿到锁执行,而是等到t2执行完释放锁,再执行
 * @ author:zhz
 * @ createTime:2021/12/1 9:51
 * @ version:1.0.0
 */
public class SleepAndWaitDemo {
    private static  Object lock = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("浮世三千,吾爱有三");
                    try {
                       // Thread.sleep(3000);
                        lock.wait();//阻塞并释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("日月与卿");
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("日为朝 月为暮");
                    lock.notify();
                    try {
                        //测试是否唤醒t1后,t1立马拿到锁,不会立马拿到
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卿为朝朝暮暮");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3 死锁和如何防止死锁

死锁成因:
当前线程拥有其他线程需要的资源
当前线程等待其他线程已拥有的资源
都不放弃自己拥有的资源

线程1和线程2 资源A和资源B 线程1 拥有 资源A 线程2拥有 资源B
线程1想拿到资源B 线程2想拿到资源A

死锁实例:


package com.aaa.mt.demo3;
/**
 * @ fileName:DeadLock
 * @ description:
 * @ author:zhz
 * @ createTime:2021/12/1 10:49
 * @ version:1.0.0
 */
public class DeadLock implements Runnable{
    private Father father =new Father();
    private Son   son = new Son();
    //标识线程是否是父亲
    private Boolean isFather=true;
    //锁
    private static Object lockA = new Object();
    private static Object lockB = new Object();
    @Override
    public void run() {
        //判断是否是父亲线程
        if(isFather){//父亲线程操作
           synchronized (lockA){
               father.say();
               try {
                   //让线程休眠给对象拿锁机会
                   Thread.sleep(200);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //获取儿子的锁
               synchronized(lockB){
                   father.get();
               }
           }
        }else {//儿子线程操作
            synchronized (lockB){
                son.say();
                try {
                    //让线程休眠给对象拿锁机会
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取父亲的锁
                synchronized(lockA){
                    son.get();
                }
            }
        }
    }
    public static void main(String[] args) {
            DeadLock deadLock1  =new DeadLock();
            deadLock1.isFather=true;
            DeadLock deadLock2  =new DeadLock();
            deadLock2.isFather=false;
            //启动线程
            new Thread(deadLock1).start();
            new Thread(deadLock2).start();
    }
}

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

4 线程池概念和作用

概念:
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
作用:

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系 统的稳定性,使用线程池可以进行统一的分配,调优和监控。

5 线程池4种常用方式

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
本质了解:
ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大线程池大小
long keepAliveTime,//线程池中超过corePoolSize数目的空闲线程最大存活时间
TimeUnit unit,//时间单位
BlockingQueue workQueue)//线程等待队列

corePoolSize,maximumPoolSize,workQueue之间关系。
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

newSingleThreadExecutor: 创建一个单线程的线程池。这个线程池只有一个线程工作。如果这个线程出现异常,会有一个新的线程来替代它。此线程保证所有的任务执行顺序是按照提交顺序执行。
corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;
适用场景:保证任务由一个线程串行执行

ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++) {
            int index=i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":"+index);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executorService.shutdown();

newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 线程池的大小一旦达到最大值就会保持不变,如果某个线程出现异常,那么会补充一个新的线程。
corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程;
workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;
FixedThreadPool的任务执行是无序的;

适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。

 ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
      int index=i;//内部类使用,在使用前不是定义
    Thread.sleep(10);
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+","+index);
        }
    });
}
executorService.shutdown();

newCachedThreadPool: 创建一个可缓存的线程池。如果线程池的大小超过处理任务所需要的线程数, 那么会回收部分空闲线程,当任务数 增加时,线程会智能添加新线程来处理任务。此线程不会对线程池的大小做限制,线程池大小完全依赖于操作系统(或 JVM)能够创建最大线程的大小。 (如果间隔时间长,下一个任务运行时,上一个任务已经完成,所以线程可以继续复用,如果间隔时间调短,那么部分线程将会使用新线程来运行。)
corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
keepAliveTime = 60s,线程空闲60s后自动结束。

workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;
适用场景:快速处理大量耗时较短的任务

ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
              int index=i;//内部类使用,在使用前不是定义
            Thread.sleep(10);
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+","+index);
                }
            });
        }
        executorService.shutdown();

newScheduledThreadPool: 创建一个无限大小的线程池。此线程池支持定时及周期性执行任务的需求。
适应场景:定时执行任务

ScheduledExecutorService scheduledExecutorService =
        Executors.newScheduledThreadPool(3);
scheduledExecutorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行");
    }
},10, TimeUnit.SECONDS);
/*scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行...");
    }
},5,2,TimeUnit.SECONDS);*/
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行1...");
    }
},1,5,TimeUnit.SECONDS);

6 线程池submit和execute区别

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

package com.aaa.mt.demo5;
import java.util.concurrent.*;
/**
 * @ fileName:MTPoolSubmitAndExecuteDemo
 * @ description:  submit和execute 区别:
 *                   1,submit即支持Runnable还支持Callable  execute 只支持Runnable
 *                   2,submit 因为支持Callable,所以就可以支持获取返回值
 *                   3,submit 因为支持Callable,所以就可以支持获取异常处理
 *  @ author:zhz
 * @ createTime:2021/12/1 11:47
 * @ version:1.0.0
 */
public class MTPoolSubmitAndExecuteDemo  {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("submit支持Runnable");
            }
        });
        Future<Object>  future= executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                System.out.println("submit支持Callable");
                System.out.println(1/0);
                return 1;
            }
        });
        System.out.println(future.get());
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("submit支持Runnable");
            }
        });
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/75501.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!