Java多线程实现方式、锁、线程池

线程与进程解析
本文深入探讨了进程与线程的概念,对比了同步与异步、并发与并行的特点,详细介绍了Java中实现多线程的三种方法及其优劣。同时,文章还讲解了线程安全、锁机制、线程池的原理及应用。

线程与进程

进程

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间

线程

是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少 有一个线程 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程

同步与异步

同步:排队执行 , 效率低但是安全.

异步:同时执行 , 效率高但是数据不安全.

并发与并行

并发:指两个或多个事件在同一个时间段内发生。

并行:指两个或多个事件在同一时刻发生(同时发生)

Java中实现多线程的3种方法介绍和比较

继承Thread类

public class MyThread extends Thread {
    //run方法就是线程要执行的任务方法
    @Override
    public void run() {
        //这里的代码就是一条新的执行路径
        //这个执行路径是触发方式,不是调用run方法,而是通过thread对象的start方法来启动任务
        for (int i = 0; i < 10; i++) {
            System.out.println("锄禾日当午"+i);
        }
    }
}

//调用方法
MyThread m = new MyThread();
        m.start();

实现Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程的任务
        for (int i = 0; i < 10; i++) {
            System.out.println("锄禾日当午"+i);
        }
    }
}

//调用方法
 //1 创建一个任务对象
MyRunnable r = new MyRunnable();
//创建一个线程并给他一个任务
Thread t = new Thread(r);
//启动线程
t.start();

实现Callable接口

public class CallableImpl implements Callable<String> {
    
    private String acceptStr;
    
    public CallableImpl(String acceptStr) {
        this.acceptStr = acceptStr;
    }

    public String call() throws Exception {
        // 任务阻塞1秒,并且增加一些信息返回
        Thread.sleep(1000);
        return this.acceptStr + " 增加一些字符并返回";
    }
    
    public static void main(String[] args) throws Exception {
        Callable<String> callable = new CallableImpl("Callable测试");
        FutureTask<String> task = new FutureTask<String>(callable);
        // 创建线程
        new Thread(task).start();
        long beginTime = System.currentTimeMillis();
        // 调用get()阻塞主线程,反之,线程不会阻塞
        String result = task.get();
        long endTime = System.currentTimeMillis();
        System.out.println("hello : " + result);
        
        // endTime 和 beginTime是不一样的,因为阻塞了主线程
        System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
    }
}

/*
输出内容===
hello : Callable测试 增加一些字符并返回
cast : 1 second!
*/

三种方法的介绍和比较

1、实现Runnable接口相比继承Thread类有如下优势

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ubipu750-1595733433694)(C:\Users\90466\AppData\Roaming\Typora\typora-user-images\image-20200725000556631.png)]

1)可以避免由于Java的单继承特性而带来的局限
2)增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
3)适合多个相同程序代码的线程去处理同一资源的情况
4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类

2、实现Runnable接口和实现Callable接口的区别

1)Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
2)实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回结果
3)Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
4)加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法
注:Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞

线程安全–锁

synchronized

Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

(1)synchronized 方法

方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

示例:

public synchronized void synMethod(){
      //方法体
  }

如在线程t1中有语句obj.synMethod(); 那么由于synMethod被synchronized修饰,在执行该语句前, 需要先获得调用者obj的对象锁, 如果其他线程(如t2)已经锁定了obj (可能是通过obj.synMethod,也可能是通过其他被synchronized修饰的方法obj.otherSynMethod锁定的obj), t1需要等待直到其他线程(t2)释放obj, 然后t1锁定obj, 执行synMethod方法. 返回之前之前释放obj锁。

(2)synchronized 块

对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.此时,线程获得的是成员锁。

(3)synchronized (this)

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
  3. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。

(4)wait() 与notify()/notifyAll()
wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()则是唤醒所有等待的线程。

Lock

在这里插入图片描述

Lock接口的实现类:ReentrantLock

构造方法:

方法名称描述
ReentrantLock()创建一个 ReentrantLock的实例。
ReentrantLock(boolean fair)创建一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例
Lock lock=new ReentrantLock();
  lock.lock();
   try{
    }finally{
    lock.unlock();
    }

synchronized和lock区别

构成不同

synchronized是关键字,属于JVM层面

Lock是具体的接口(java.util.concurrent.locks.lock),属于api层面(一般我们使用它的实现类ReentrantLock)

使用方式

sychronized不需要用户手动释放锁,当sychronized代码执行完后系统会自动让线程释放对锁的占用

ReentrantLock需要用户手动去释放锁,若没有主动释放可能出现死锁的情况。

等待是否可中断

synchronized不可中断,除非抛出异常或正常运行完成

ReentrantLock可中断,可以设置超时方法trylock(TimeUnit unit)、可设置中断方法interrupt()中断

加锁是否公平

synchronized只能使用非公平锁

ReentrantLock两者都可以,可以设置参数true为公平锁,false或不填为默认非公平锁

锁绑定多个条件Condition

synchronized不能绑定多个条件,要么唤醒一个要么唤醒全部线程

ReentrantLock用来实现分组唤醒与精准唤醒

线程池

1、线程池的优势

(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

(4)提供更强大的功能,延时定时线程池。

2、线程池的主要参数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。

5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

5、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。

3、线程池流程

img

1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

(1、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。)

Java中的四种线程池

img

1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

/**     * 缓存线程池.      
* (长度无限制)     
* 执行流程:     
*      1.  判断线程池是否存在空闲线程     
*      2.  存在则使用     
*      3.  不存在,则创建线程 并放入线程池, 然后使用     
*/    
ExecutorService service = Executors.newCachedThreadPool();    
//向线程池中 加入 新的任务    
service.execute(new Runnable() {        
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
});    
service.execute(new Runnable() {       
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
});    
service.execute(new Runnable() {        
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    });

2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

 /**     
 * 定长线程池.      
 * (长度是指定的数值)     
 * 执行流程:
 *      1.  判断线程池是否存在空闲线程     
 *      2.  存在则使用     
 *      3.  不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用     
 *      4.  不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程     
 */
ExecutorService service = Executors.newFixedThreadPool(2);    
service.execute(new Runnable() {        
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
});    
service.execute(new Runnable() {       
    @Override        public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
})

3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

//效果与定长线程池 创建时传入数值1 效果一致.
    /**     
    * 单线程线程池.      * 执行流程:     
    *      1.  判断线程池 的那个线程 是否空闲     
    *      2.  空闲则使用     
    *      3.  不空闲,则等待 池中的单个线程空闲后 使用     
    */    
ExecutorService service = Executors.newSingleThreadExecutor();    
service.execute(new Runnable() {        
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
});    
service.execute(new Runnable() {        
    @Override        
    public void run() {            
        System.out.println("线程的名称:"+Thread.currentThread().getName());        
    }    
});

4、newScheduledThreadPool:适用于执行延时或者周期性任务。

public static void main(String[] args) {    
    /**     
    * 周期任务 定长线程池.      
    * 执行流程:     
    *      1.  判断线程池是否存在空闲线程     
    *      2.  存在则使用     
    *      3.  不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用     
    *      4.  不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程     
    * 周期性任务执行时:      
    *      定时执行, 当某个时机触发时, 自动执行某任务 .
     */
    ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
    /**     
    * 定时执行     
    *  参数1.    runnable类型的任务     
    *  参数2.    时长数字     
    *  参数3.    时长数字的单位     
    */    
    /*service.schedule(new Runnable() {
        @Override        
        public void run() {            
        System.out.println("俩人相视一笑~ 嘿嘿嘿");        
        }    
        },5,TimeUnit.SECONDS);    
        */    
    /**     * 周期执行     
    *  参数1.    runnable类型的任务     
    *  参数2.    时长数字(延迟执行的时长)     
    *  参数3.   周期时长(每次执行的间隔时间)     
    *  参数4.    时长数字的单位     */    
    service.scheduleAtFixedRate(new Runnable() {
        @Override        
        public void run() {            
            System.out.println("俩人相视一笑~ 嘿嘿嘿");        
        }    
    },5,2,TimeUnit.SECONDS);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值