回顾线程的基本内容

程序:静态代码 安装在硬盘上的。
进程:运行的程序,是操作系统分配内存的空间单位。
线程:是进程中的最小执行单位,是cpu的调度单位,线程依赖于进程。

创建线程

  1. 线程类 继承Thread
  2. 类 实现Runnable接口 重写run()
    创建Thread类的对象,为其分配任务。
  3. 类 实现 Callable接口 重写call()方法 有返回值 可以抛出异常。
    创建Thread类的对象,为其分配任务。

常用的方法
run()
call()
start()

线程状态
start() 就绪态
join() 阻塞
sleep() 阻塞
yield() 线程让步 让线程从运行状态主动让出—>进入就绪状态。

多线程
程序中如果有多个任务执行,就需要多线程。
多线程可以提高程序的执行效率。
提高CPU的利用率

不足:
对CPU的要求增加了
多线程同时访问同一个共享资源

线程安全问题:
多线程 且 多个线程访问一个共享数据。
解决办法:排队 + 锁

  • Syschronized
    关键字 修饰方法 修饰一个代码块 是隐式锁 自动加锁 自动释放锁。
  • ReentrantLock
    只能对某段代码加锁 是显示锁,手动添加 手动释放。

守护线程
死锁
线程通信
wait()
notify()
sleep() 休眠指定的时间 时间到了之后,就会进入到就绪态 不会释放锁。
wait() 让线程等待 必须要通过notify()唤醒 等待时会释放锁。

并发编程

并发编程是什么?

  • 并发:在某一段时间内,依次做好几件事 (一个一个做)

多个线程访问同一个资源
单核CPU不会出现问题
随着CPU的功能主键强大,已经发展到多核,意味着可以同时执行多个线程,这样就有可能多个线程同时访问同一个共享数据。(秒杀抢购)
并发编程就是要让在多核多线程的情况下,也只能一次只有一个线程对共享数据进行访问。
我们现在已经可以通过加锁的方式实现,但是枷锁并不是唯一的解决方案,还有其他的方式来实现。

  • 下面就说说并发问题如何产生,如何解决,学习新的方式解决,以及加锁的内部原理问题。
    多核cpu可以真正的做到哦并行执行,但是我们在某种场景下就是要让程序并发执行。
  • 并行:在一个时间节点,可以同时做很件事情

多线程的优点:

提高程序的性能,可以同时执行多个任务。
榨取硬件的剩余价值。

多线程的不足:

安全性(访问共享变量)
性能(CPU要切换线程)

并发中出现问题的原因:
cpu,内存,硬盘三者之间的读写速度是有差异的。(CPU>内存>硬盘)
系统为了解决此类问题,做了一些优化
cpu增加了缓存功能。
可以优化代码的执行顺序。(有些时候优化是会产生问题的)
缓存中和内存中的数据不一致。
可能会影响后续代码的执行结果。

Java内存模型(JMM)

注意是Java内存模型(JMM)

  • Java多线程在工作时,对数据进行操作,操作完成后,再将数据写回到主内存。
  • 这样就会产生可见性问题

B线程看不到A线程中操作过的数据。
操作系统可能将会对指令的执行先后顺序进行重新排列执行。这样就会带来有序性的问题。

  • 线程切换带来原子性(不可拆分)问题

并发编程核心问题:

  • 线程本地缓存 会导致可见性问题。
  • 编译优化重拍指令 会导致有序性问题。
  • 线程切换执行 会导致原子性问题。

如何解决问题

  • volatile关键字

    1. 被volatile修饰后的共享变量在一个线程操作后,就可以保证在另一个线程中立即可见。2. 被volatile修饰后的共享变量,禁止优化重新排序3. volatile不能保证对变量操作的原子性。

案例测试

public class ThreadDemo implementsRunnable{/* volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见禁止cpu对指令重排序 */private volatile boolean flag = false;//共享数据@Overridepublic void run() {try {//防止此线程先执行后将flag状态修改后main方法才执行Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}this.flag = true;//让一个线程修改共享变量值System.out.println(this.flag);}public boolean getFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}}
public class TestVolatile {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();Thread t = new Thread(td);//创建线程t.start();//main线程中也需要使用flag变量while(true){if(td.getFlag()){System.out.println("main---------------");break;}}}}

不加volatile的运行效果

  • main看不到另一个线程修改flag会一直死循环。

    加volatile的运行效果
  • main 能看到另一个线程修改flag

    如何保证原子性
    加锁
    • Lock ReentrantLock
    • Synchronized

    原子类
    Java.util.concurrent包
    Atomiclnteger 通过volatile +CAS实现原子操作的。

案例测试

  • 一般情况
public class ThreadDemo implements Runnable{ privateint num = 0;//共享变量@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":::"+getNum());}public int getNum() {return num++; }}
public class Test {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();for (int i = 0; i <10 ; i++) {//循环创建10个线程Thread t = new Thread(td);t.start();}}}

  • 加volatile关键字
public class ThreadDemo implements Runnable{private volatile int num = 0;@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":::"+getNum());}public int getNum() {return num++;}}
public class Test {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();for (int i = 0; i <10 ; i++) {//循环创建10个线程Thread t = new Thread(td);t.start();}}}

volatile关键字不能保证原子性:

  • 通过原子类(可以解决)
    java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo implements Runnable{ private AtomicInteger num = new AtomicInteger(0);@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":::"+getNum());}public int getNum() {return num.getAndIncrement();}}
public class Test {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();for (int i = 0; i <10 ; i++) {//循环创建10个线程Thread t = new Thread(td);t.start();}}}

CASS(Compare-And-Swap)
比较并交换,该算法是硬件对于并发操作的支持适用于低并发的情况。
CAS是乐观锁的一种实现方式,它采用的是自旋锁的思想,是哟中轻量级的锁机制。

  • 乐观锁 指的是一种不加锁就可以就解决的方式

  • 自旋锁(轮巡)一直不停的循环检查。

    CAS包含了三个操作数1. 内存值V2. 预估值A(比较时,从内存中再次读到的值)3. 更新值B(更新后的值)当且仅当预期值A==V,将内存值V=B,否则什么都不做。

这种做法不会导致线程阻塞,因为没有加锁。

缺点:

  • 在高并发的情况下,采用自旋方式去不断循环,会提高对CPU的占用率。
  • 可能会产生ABA问题,可以为类添加版本号的方式去区别值是否被修改过。

ABA问题就是虽然线程1第一次拿到的值与第二次拿到的值是一样的,但是第二次拿的的值是线程2更改过的,只不过最后的结果和原来的值一样。


JUC常用类

HashMap线程不安全Hashtable线程安全ArrayList线程不安全Vecotor 线程安全

在特别高的并发的情况下Hashtable Vector虽然是线程安全的,但是效率低。

JUC包下提供了一些针对于高并发情况下使用的类,安全性是可靠的,效率相对于Hashtable Vector效率有所提高。

/*HashMap 是线程不安全的ConcurrentModificationException 并发修改异常 */public class HashMapDemo {public static void main(String[] args) {HashMap<String,Integer> hashMap=new HashMap<>();for (int i = 0; i < 20; i++) {new Thread(()->{hashMap.put(Thread.currentThread().getName(),new Random().nextInt());System.out.println(hashMap);}).start();}}}


Hashtable与ConcurrentHashMap

  • Hashtable直接在put方法上加锁,效率较低,用在低并发情况下可以。

  • ConcurrentHashMap采用分段锁机制,并没有将整个hash表锁住,jdk8之后就再也没有使用分段锁(给每个位置创建一个锁标志对象)。
    采用的是CAS思想+synchronized来实现

插入时,检测hash表对应的位置是否是第一个节点,如果是采用CAS机制(循环检查)向第一个插入位置插入数据。
如果此位置有值,那么就以第一个Node对象为锁的标志进行加锁,使用的是synchronized实现。

CopyOnWriteArrayList与ArrayList与Vector

  • ArraysList是线程不安全的,在高并发的情况下可能会出现问题。
  • Vector虽然是线程安全的,但是其读和写操作都加了锁,这是一种资源的浪费,在读取时,我们应该允许多个线程同时访问,因为读操作是线程安全的。


  • CopyOnWriteArrayList类将读取的性能发挥到极致,取是完全没有加锁的,更厉害的是,写入也不阻塞读取的操作,只有写入和写入之间需要同步等待,读取的性能的到了大幅提升。

添加时先将原数组复制出一个副本
然后将数据添加到副本中
最后再用副本 替换原数组


CopyOnWriteArraySet

  • CopyOnWriteArraySet 的实现基于CopyOnWriteArrayList,不能存储重复数据.

辅助类CountDownLatch与辅助类CyclicBarrier

  • CountDownLatch 这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为 0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

  • 如果计数器的值大于线程个数,那么就会死锁。

public class CountDownLatchDemo {/*CountDownLatch 辅助类使一个线程 等待其他线程执行结束后在执行相当于一个线程计数器,是一个递减的计数器先指定一个数量,当有一个线程执行结束后就-1,直到为零就关闭计数器。这样线程就可以执行了 */public static void main(String[] args) throws InterruptedException {CountDownLatch downLatch = new CountDownLatch(6);//计数for (int i = 0; i < 6; i++) {new Thread(()-> {System.out.println(Thread.currentThread().getName());downLatch.countDown();//计数器减一操作}).start();}downLatch.await();//关闭计数器System.out.println("main线程执行");}}

  • CyclicBarrier 是一个同步辅助类,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门。
public class CyclicBarrierDemo {/*CyclicBarrier 让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门放行 */public static void main(String[] args) {CyclicBarrier c = new CyclicBarrier(5,() -> {System.out.println("大家都到了,该main执行了");});for (int i = 0; i < 5; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName());try {c.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}).start();}}}

有许多锁的名词,有的指锁的特性、有的指锁的设计、有的指锁的状态。

  • 乐观锁

乐观锁就是不加锁,任务并发的修改是没有问题的,例如CAS机制。

  • 悲观锁

悲观锁认为并发操作会出现问题,需要通过加锁来保证安全。

  • 可重入锁

当一个同步方法中调用另一个和他使用同一把锁的方法时,在外层方法中,即使锁没有释放的情况下,也可以进入另一个同步方法。
Synchronized ReentrantLock都是可重入锁

public class RepeatDemo {synchronized void setA(){System.out.println("调用A方法");setB();}synchronized void setB(){System.out.println("调用B方法");}public static void main(String[] args) {new RepeatDemo().setA();}}

如果不是可重入锁,setB()是不会被当前线程执行的,会造成死锁。

读写锁

  • 读写两把锁,进行分类使用
  • 可以多个线程同时读。
  • 写互斥(只允许一个线程写,也不能读写同时进行)
  • 写优先于读(一旦有写,则读必须等待,唤醒时优先可虑写线程)。

分段锁

不是一种锁的实现,是一种加锁的思想,采用分段加锁,降低所得粒度,从而提高效率。

自旋锁

不是一种锁的实现,采用自旋(循环重试)的方式进行尝试获取执行权,不会让线程进入阻塞状态,适合用于锁时间较短的情况。

共享锁

可以被多个线程共享的锁
ReadWriteLock 的读锁是共享的,多个线程可以同时读数据。

独占锁

一次只有一个线程获取锁。
ReentrantLock 、synchronized、ReadWriteLock 写锁都独占锁。
ReadWriteLock 里面的实现方式 使用一个同步队列,例如读线程获取资源,将标准state设置为已被使用,然后将其他线程加入到一个队列中等待。

AQS(AbstractQueuedSynchronizer)

维护了一个队列,让等待的线程排队。

公平锁

就是会维护一个线程的等待队列,依次去执行线程。
ReentrantLock默认是非公平,可以在创建时通过构造器方法为其指定是公平锁还是非公平锁。

非公平锁

没有队列,一旦锁释放,线程就开始抢占,谁抢到执行权谁就先制性,synchronized就是非公平的。

锁的状态
无所状态、偏向锁状态、轻量级锁状态、重量级锁状态。
锁的状态是通过对象监视器在对象头的字段来表明的。

  • 偏向锁:一直只有一个线程在不断获取锁,可以更方便的获取到锁。

  • 轻量级锁:当是偏向锁时,再有一个线程来访问,那么锁状态升级为轻量级锁。
    如果是轻量级锁,那么等待的线程不会进入阻塞状态,采用自旋方式,重新尝试获取锁,效率提高。

  • 重量级锁:当锁状态是轻量级锁时,如果有的线程自旋次数过多,或者有大量的线程访问,那么锁的状态升级为重量级锁,此时为获得锁的线程不再自旋,进入阻塞状态。

对象头
在 Hotspot 虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java 对象头是实现 synchronized 的锁对象的基础,一般而言,synchronized 使用的锁对象是存储在 Java 对象头里。它是轻量级锁和偏向锁的关键。

synchronized

  • 是可重入锁,非公平锁。
  • 是关键字,可以修饰代码块,也可以修饰方法。
  • 是隐式的 自动获取 释放锁
  • synchronized 实现加锁 释放锁 是指令级别的
  • 有一个进入监视器和退出监视器

有个线程进入 进入监视器 +1 对象头锁标记已被使用。
执行完退出 退出监视器-1 0 对象头标记改为无锁。

ReentrantLock

  • 可重入锁 可以是公平锁也可以是非公平锁。
  • 是类,只能修饰代码块 是显示的 手动添加 手动释放
  • 是类层面实现控制的,采用CAS和AQS队列实现的。
  • 在类的内部维护了一个锁的状态,一旦有线程抢占到了,将状态改为1,其他线程进入到队列中等待锁释放,锁一旦释放,那么就唤醒头结点,开始去唤醒锁。

线程池

池 数据库连接池 每次与数据连接 创建连接对象Connection 操作完之后 ,进行销毁 频繁创建雄安会比较耗时。
创建一个池子,预先在池子中初始化好一部分里连接对象,使用时直接获取即可,用完还回,不需要频繁创建销毁。

为什么使用线程池?
并发量较大的情况下,频繁的创建销毁线程开销较大,创建一个线程可以缓解压力。

在JDK5之后,提供了ThreadPoolExecutor类来实现线程池的创建,是建议被使用的,里面有七个参数用来设置对线程池的特征的定义。

构造器中各个参数的含义

  • corePoolSize:核心线程池数量,在创建后核心线程池数量默认为零,有任务来了后才会去创建线程去执行,或者就是去调用prestartAllCoreThreads()或者prestartCoreThread()方法进行欲创建。
  • maximumPoolSize:线程池的总数,表示线程池大能装多少个线程。
  • keepAliveTime:指的是非核心线程池中的线程,在没有执行时,空闲多长时间内销毁。
  • unit:为keepAliveTime定义时间单位。

  • workQueue等待队列 可以自己来指定等待队列的实现类。
  • threadFactory:线程工厂,主要用来建设线程;
  • handler:表示当拒绝处理任务时的策略。

线程池的执行

当有任务提交的线程池时,

  1. 首先检查核心线程池是否已满
    若核心线程池未满:则在核心线程池中创建一个线程处理。
    若核心线程池已满:就去检查等待队列
    2.如果等待队列已满:就再检查线程池是否已满
    如过线程池已满,等待队列也满了,就创建非核心线程来处理任务。
    3.如果任务继续增多,核心线程池、等待对列、非核心线程池都已满,那么就使用响应了拒绝策略处理

线程池中的队列
线程池中有以下工作队列

  • ArrayBlockingQueue:有界队列,是用数组实现的有解阻塞队列按FIFO排序量。
    LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列按FIFO排序任务,容量可以选择进行设置,不设置的话将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene。

线程池拒绝策略

  • AbortPolicy策略:抛出异常。
  • DiscardOleddestPolicy策略:该策略将丢弃最老的一个请求。
  • DiscardPolicy策略:该策略丢弃无法处理的任务,不与任何处理。
  • CallerRunsPolicy:让提交任务的线程去执行,例如main方法

execute 与 submit 的区别

  • execute 适用于不需要关注返回值的场景。
  • submit 方法适用于需要关注返回值的场景。

关闭线程池
关闭线程池可以调用shutdowmNow 和shutdowm两个方法来实现。

  • shutdowmNow:立即终止线程任务。
  • shutdowm :不接收新的任务,执行完任务后关闭。

创建线程的4种方法

  1. 继承Thread
  2. 实现Runnable
  3. 实现Callable
  4. 使用线程池

ThreadLocal

线程变量
为每一个线程去保存一个变量副本,使得多个线程变量之间相互不影响。

ThreadLocal原理分析
创建一个ThreadLocal对象
调用set方法时,会在底层获取到当前正在执行的对象为我们的当前线程创建ThreadLocalMap对象,ThreadLocalMap的键时ThreadLocal对象,值就是我们自己set的值。

找的时候,先通过我们自己线程去找对象的ThreadLocalMap

 Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);

ThreadLocalMap 内存泄漏问题
内存泄漏:有些对象在内存中已经不被使用了,但是不能被回收。


ThreadLocalMap 的键是ThreadLocal,是一个被弱引用对象管理的。
键是弱引用,如果长时间的线程执行,键就被回收了为null,但是value是强引用,不能被回收掉,造成内存泄漏。

解决办法:在ThreadLocal中的变量被使用完之后要立即将其删除。