Java多线程知识点回顾

毋庸置疑,多线程是一个程序员必备的技能项,但是想要去深入理解它的难度也很大,本篇对多线程的重点内容进行回顾,希望能帮助到一些同学对于多线程的理解。解答的内容经过本人筛选,从网上大量的信息挑选出最适宜的解答。

进程和线程的区别及关系


http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html这篇博文总结的非常好,简单明了。

多线程一定比单线程快吗


不一定,因为多线程中线程的创建和上下文切换也需要消耗时间。如果并发执行的操作不多的话,多线程速度会比单线程执行的慢。

线程的创建


有两种方式创建新的线程。第一种是定义线程类实现Runnable接口,第二种是定义一个Thread的子类并重写其run方法。
两个方法比较而言第二个方法代码量较少,但是第一个方法比较灵活,自定义线程类还可以继承其他的类,而不限于Thread类,推荐使用接口来实现多线程

线程的状态


新生状态:用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态

就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列,等待系统为其分配CPU。

运行状态:在运行状态的线程执行自己的run方法中的代码,直到调用其他方法而终止、或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片没有执行结束,就会被系统给换下来回到等待执行状态

阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列,只有当引起阻塞的原因消除时,如睡眠时间已到,或等待I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列排队等待,被系统选中后从原来停止的位置开始继续运行

死亡状态:线程生命周期的最后一个阶段,原因有两个,一个是正常运行的线程完成了它的全部工作,另一个是线程被强制性的终止,如通过执行stop或destroy方法来终止一个线程

线程控制基本方法


isAlive()判断线程是否还活着,即是否还未终止
getPriority()获得线程的优先级数值
setPriority()设置线程的优先级数值
Thread.sleep()将当前线程睡眠指定毫秒数
join()调用某线程的该方法,将当前线程与该线程“合并”,即等待该线程结束,再恢复当前线程的运行
yield()让出CPU,当前线程进入就绪队列等待调度
wait()当前线程进入对象的wait pool

notify()

notifyAll()

唤醒对象的wait pool 中的一个/所有等待线程

sleep 是静态方法,不需要new出Thread对象就可以执行,会抛出异常(throws InterruptedException),这个异常是睡眠时被打断的异常,所以必须写try catch,并且sleep方法执行过程中获得当前对象的锁,其他线程无法访问当前资源。适用于几个场合:一个是倒计时,间隔n秒打印一个数字,另外一个是模拟网络延时。

wait()和sleep()的区别:

  • wait()是Object的方法,而sleep()是Thread的静态方法
  • wait()会释放锁,sleep()不会

线程同步


多个线程访问同一个资源,多个线程可能会产生不一致。为了解决此问题,需要线程同步。调用某个方法时,该资源是被一个线程独占,不能被其他线程访问。不能出现一个线程在访问一个资源的时候被另一个线程打断

MyThread m = new MyThread();
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();

这是两个线程,如果run方法中调用了一个类的一个方法,那么这两个线程会如何进行呢?两个线程执行时访问了同一个对象的同一个方法,那么该方法资源被两个线程共享,两个线程交替执行该方法。一个线程修改了该方法的变量的值,下一个线程再调用这个方法会接着第一个线程的最终结果继续调用。但这不是我们想要的结果。问题出在第一个线程执行这个方法过程中被第二个线程打断了,执行这个方法就应该从头到尾地直至执行完毕(原子性地执行)

在一个线程访问对象时,将该对象锁住,只能被该线程使用而不能被其他线程使用

线程同步方法1:
在Java语言中,引入了对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象。关键字synchronized来与对象的互斥锁联系。当某个对象synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

线程同步方法2:
使用锁对象 ReentrantLock

synchronized


参照:https://blog.csdn.net/zjy15203167987/article/details/82531772

实现原理:synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

三种应用方式:Java中每一个对象都可以作为锁,这是synchronized实现同步的基础

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁 
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 

synchronized作用:

  1. 确保线程互斥的访问同步代码 
  2. 保证共享变量的修改能够及时可见 
  3. 有效解决重排序问题。

ReentrantLock


// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)

// 查询当前线程保持此锁的次数。
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
ReentrantLock myLock = new ReentrantLock();
myLock.lock();
try{
} finally{
  mylock.unlcok();
}

将释放锁的代码放在finally中是非常重要的,引入如果在临界区抛出异常,那么锁必须被释放,否则其他线程会一直阻塞

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

重入的实现:

reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。线程在每一次调用lock都要调用unlock来释放锁,由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。比如在转账时(锁住)打印总额(锁住),就获得了锁两次。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

用两个线程在控制有序打出1,2,3
public class ReentrantLockThread implements Runnable{
    // 创建一个ReentrantLock对象
    ReentrantLock lock = new ReentrantLock();
    public void run(){
        try{
            lock.lock();
            for (int i = 0; i < 3; i++){
                System.out.println(Thread.currentThread().getName() + "输出了:" + i);
            }
        }finally{
            // 别忘了执行unlock()方法执行锁
            lock.unlock();
        }
    }
}
public class FirstReentrantLock{
    public static void main(String[] args){
        Runnable runnable = new ReentrantLockThread();
        new Thread(runnable, "a").start();
        new Thread(runnable, "b").start();
    }
}
a输出了: 0    
a输出了: 1 .....

 

来源:https://www.cnblogs.com/xiaoxi/p/7651360.html

Condition


Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。

Condition类能实现synchronized和wait、notify搭配的功能,另外比后者更灵活,Condition可以实现多路通知功能,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活。而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在这个对象上。线程开始notifyAll时,需要通知所有的WAITING线程,没有选择权,会有相当大的效率问题。

  1. Condition是个接口,基本的方法就是await()和signal()方法
  2. Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
  3. 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
  4. Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()

使用Condition来实现等待/唤醒,并且能够唤醒制定线程
public class MyService {
    // 实例化一个ReentrantLock对象
    private ReentrantLock lock = new ReentrantLock();
    // 为线程A注册一个Condition
    public Condition conditionA = lock.newCondition();
    // 为线程B注册一个Condition
    public Condition conditionB = lock.newCondition();
    public void awaitA(){
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"进入了awaitA方法");
            long timeBefore = System.currentTimeMillis();
            //执行conditionA等待
            conditionA.await();
            long timeAfter = System.currentTimeMillis();
            sout(Thread.currentThread().getName()+"被唤醒");
            sout(Thread.currentThread().getName()+"等待了"+(timeAfter-timeBefore)/1000+"s");
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
}
    publick void awaitB(){.....如上}
    public void signallA(){
        try{
            lock.lock();
            sout("启动唤醒程序");
            conditionA.signall();
        }finally{lock.unlock();}}
    public void signallB(){......}
    

分别实例化两个Condition对象,都是使用同一个lock注册。注意conditionA对象的等待和唤醒只对使用了conditionA的线程有用,同理conditionB对象的等待和唤醒只对使用了conditionB的线程有用。

public class MyServiceThread1 implements Runnable{
    private MyService service;
    public MyServiceThread1 (MyService service){
        this.service = service;
    }
    public void run(){
        service.awaitA();
    }
}    // myServiceThread1使用了awaitA()方法,持有的是conditionA  myServiceThread2使用awaitB()
public class ApplicationCondition {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        Runnable runnable1 = new MyServiceThread1(service);
        Runnable runnable2 = new MyServiceThread2(service);
        new Thread(runnable1, "a").start();
        new Thread(runnable2, "b").start();
        Thread.sleep(2000); // 线程sleep2秒钟
        service.signallA(); // 唤醒所有持有conditionA的线程
        Thread.sleep(2000); //
        service.signallB(); }}
//控制台输出
a进入了awaitA方法
b进入了awaitB方法
启动唤醒程序
a被唤醒
a等待了:2s
启动唤醒程序
b被唤醒
b等待了:4s

使用conditionA的线程被唤醒,而后再唤醒使用conditionB的线程。学会使用Condition,可用它来实现生产者消费者模式

Lock与synchronized的区别


  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类

  2. ReentrantLock提供了synchronized类似的功能和内存语义

  3. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁

  4. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁

  5. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了,不容易产生死锁

  6. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

  7. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

  8. 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票

  9. ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合

Volatile


如果声明一个成员变量是 volatile 的,那么会通知编译器和虚拟机这个成员变量是可能其他线程并发更新的,会保证从线程的共享内存加载到线程工作内存是最新的,不会从线程工作缓存中读值。主要是为了安全地读取这个值,volatile 变量不具有原子性。

jvm运行时刻内存的分配:其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,(从线程内存中读值) 在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

但是这些操作并不是原子性,也就是 read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

同步格言:如果向一个变量中写入值,而这个变量接下来可能被另一个线程读取;或者,从一个变量中读值,而这个变量可能之前被另一个线程写入的,那么必须使用同步

内存可见性问题是,当多个线程操作共享数据时,彼此不可见
解决这个问题又两种方法:
1、加锁:加锁会保证读取的数据一定是写回之后的,内存刷新,但是效率较低
2、volatile:会保证数据在读操作之前,上一次写操作必须生效,即写回
      1) 修改volatile 变量时会强制将修改后的值刷新主内存中
      2) 修改volatile 变量后会导致其他线程工作内存中对应的变量值失效。因此再读取该变量值的时候就需要重新从内存中读取值
相较于synchronized是一种较为轻量级的同步策略,但是volatile不具备互斥性,不能保证变量的原子性

公平锁 非公平锁 乐观锁 悲观锁 自旋锁 偏向锁 可重入锁 轻量级锁 独享锁 共享锁 重量级锁


锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized(关键字,jvm)和ReentrantLock(可重入锁,来自jdk,可看源码)等独占锁就是悲观锁思想的实现。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

悲观锁:假设一定会发生并发冲突,通过阻塞其他所有线程来保证数据的完整性。
乐观锁:假设不会发生并发冲突,直接不加锁去完成某项更新,如果冲突就返回失败。

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于synchronized而言,当然是独享锁。

可重入锁:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于Java ReentrantLock而言, 其名字是Re entrant Lock即是重新进入锁。对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

线程安全


线程安全性的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程池


合理使用线程池能够带来三个好处:

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

线程池的实现原理:

当线程池提交一个任务后,线程池是如何处理这个任务的?

从图可以看出,当提交一个新任务到达线程池时,处理流程如下

  1. 线程池判断核心线程池的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程
  2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列满了,则进入下个流程
  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务

ThreadPoolExecutor执行execute()方法的示意图如下

ThreadPoolExecutor执行execute方法分为下面4中情况

  1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁)
  2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步需要获取全局锁)
  4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法

线程池的使用

线程池的创建:
可以通过ThreadPoolExecutor来创建一个线程池

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler);

创建一个线程池时需要输入几个参数,如下

  1. corePoolSize (线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空间的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程
  2. runnableTaskQueue (任务队列):用于保存等待执行的任务的阻塞队列。可选择以下几个阻塞队列
    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列
    .PriorityBlockingQueue:一个具有优先级的无限阻塞队列
  3. maximumPoolSize (线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果
  4. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字
  5. RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在jdk 1.5中java线程池框架提供了以下4种策略
    *AbortPolicy:直接抛出异常
    *CallerRunsPolicy:只用调用者所在线程来运行任务
    *DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
    *DiscardPolicy:不处理,丢弃掉
    当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
    *keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存货的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率
    *TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒、微妙、纳秒

向线程提交任务

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例

threadsPool.execute(new Runnable(){
                        @Override
                        public void run(){
                            // TODO Auto-generated method stub
                        }
            });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而是用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完

Future<Object> future = executor.submit(harReturnValuetask);
                try{
                        Object s = future.get();
                } catch (InterruptedException e) {
                    //处理中断异常
                } catch (ExecutionException e){
                    //处理无法执行任务异常
                } finally {
                    //关闭线程池
                    executor.shutdown();
                } 

关闭线程池:可以调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

合理配置线程池
任务的性质:CPU密集型任务、IO密集型任务和混合型任务 *任务的优先级:高、中和低 *任务的执行时间:长、中和短 *任务的依赖性:是否依赖其他系统资源,如数据库连接

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型配置尽可能小的线程,IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程

生产者和消费者


参照:https://www.cnblogs.com/xiaoxi/p/7910868.html

连续打印abc的几种解法


https://www.cnblogs.com/xiaoxi/p/8035725.html

死锁


所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。先看生活中的一个实例,两个人面对面过独木桥,甲和乙都已经在桥上走了一段距离,即占用了桥的资源,甲如果想通过独木桥的话,乙必须退出桥面让出桥的资源,让甲通过,但是乙不服,为什么让我先退出去,我还想先过去呢,于是就僵持不下,导致谁也过不了桥,这就是死锁。

避免死锁的方式:

  1. 让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实
  2. 设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量
  3. 既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1。此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。

读写锁

ReentrantReadWriteLock 如果很多线程从一个数据结构中读取数据而很少线程修改其中数据,那么允许对读的线程共享访问是合适的。读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁

线程进入读锁的前提条件:没有其他线程的写锁,没有写请求或者有写请求,但调用线程和持有锁的线程是同一个(排斥写操作)
线程进入写锁的前提条件:没有其他线程的读锁    没有其他线程的写锁   (排斥读操作和写操作)

到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。然后就是总结这个锁机制的特性了:

     (a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。

     (b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a)

     (c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。

     (d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。

     (e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。

阻塞队列 BlockingQueue

一、什么是BlockingQueue
BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

  1. 当队列满了的时候进行入队列操作
  2. 当队列空了的时候进行出队列操作

因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。

二、BlockingQueue的用法
阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:

来源: https://www.jb51.net/article/142626.htm

负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到这个队列的上限值。队列达到上限值之后生产线程将会被阻塞,直到消费的线程对这个队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,当队列为空时,消费线程将会被阻塞,除非队列中有新的对象被插入。

三、BlockingQueue的核心方法

插入方法:

  • add(E e) : 添加成功返回true,失败抛IllegalStateException异常
  • offer(E e) : 成功返回 true,如果此队列已满,则返回 false
  • put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞

删除方法:

  • remove(Object o) :移除指定元素,成功返回true,失败返回false
  • poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
  • take():获取并移除此队列头元素,若没有元素则一直阻塞

四、BlockingQueue的实现类

  1. ArrayBlockingQueue:是一个有边界的阻塞队列,内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。另外它以FIFO先进先出的方式存储数据,最新插入的对性爱那个是尾部,最新移除的对象是头部。有点需要注意的是 ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序
  2. LinkedBlockingQueue:是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,如果需要的话,这一链式结构可以自定义一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。建议指定队列大小,默认大小在添加速度大于删除速度情况下可能造成内存溢出,LinkedBlockingQueue队列也是按 FIFO(先进先出)排序元素。LinkedBlockingQueue中维持两把锁,一把锁用于入队,一把锁用于出队,这也就意味着,同一时刻,只能有一个线程执行入队,其余执行入队的线程将会被阻塞。它使用一个AtomicInterger类型的变量表示当前队列中含有的元素个数,所以可以确保两个线程之间操作底层队列是线程安全的
  3. DelayQueue
  4. PriorityBlockingQueue
  5. SynchronousQueue

手写一个BlockingQueue吧,实现它的put和get方法(用Reentrantlock的condition实现)
用BlockingQueue写一个生产者消费者模式


 

 

 

 

 


参考:
https://www.cnblogs.com/iyyy/p/7993788.html
Java并发编程的艺术 作者:方腾飞等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值