Java多线程学习笔记

一、逻辑理解:

1.多线程主要讲什么

使用多线程是为了提高效率,让多个线程并行地执行,而多线程主要要解决的问题就是要让并发的线程之间能有有效的共享资源以及能相互合作

明确两个方向:线程之间的关系与共享资源与线程的关系,这两个方向都是以访问共享资源为核心的。

(1)线程之间存在两种关系:互斥与同步,互斥是一种特殊的同步。

  • 互斥关系就是一个线程在访问一个共享资源的时候,其他线程都得等待这个线程。
  • 同步关系就是多个线程之间存在合作的关系,即有一个先后的顺序。

(2)从共享资源的角度出发,线程对资源的操作也有两种。

  • 多个线程行为一致地操作同一个共享数据
  • 多个线程行为不一致地操作同一个共享数据

(3)随着线程孕育而生的事物

  • 线程的状态和基础使用
  • 管理多个线程——线程池
  • 互斥同步的实现——
  • 对共享资源安全的访问——装载共享资源的并发队列(阻塞队列与非阻塞队列)
  • 让线程之间相互合作——同步器,wait()方法等

2.线程的状态和基础使用:

简图

详图

线程共包括以下6种状态

  • New:新建状态

可以通过实现Runnable接口或继承Thread声明一个线程类,new一个实例后,线程就进入了新建状态。eg:new Thread();

  • Runnable:可运行状态=Ready就绪状态 + Running运行状态

线程对象创建成功后,调用该线程的start()函数,线程进入就绪状态,该状态的线程进入可运行线程池中,等待获取CPU的使用权。

此时线程调度程序正在从可运行线程池中选择一个线程,该线程进入运行状态。换句话说,线程获取到了CPU时间片。当线程时间片用完或调用的yield()函数,该线程回到就绪状态。

yield()函数的作用是让步,让当前线程由“运行状态”进入到“就绪状态”。

  • Terminated:终止状态

线程继续运行,直到执行结束或执行过程中因异常意外终止都会使线程进入终止状态。

线程一旦终止,就不能复生,这是不可逆的过程。

  • Waiting:等待状态

运行状态的线程执行wait()、join()、LockSupport.park()任意函数,该线程进入等待状态。

其中wait()join()函数会让JVM把该线程放入锁等待队列。

处于这种状态的线程不会被分配C P U执行时间,它们要等待被主动唤醒,否则会一直处于等待状态。

执行LockSupport.unpark(t)函数唤醒指定线程,该线程回到就绪状态。

而通过notify()、notifyAll()、join线程执行完毕方式,会唤醒锁等待队列的线程,出队的线程回到就绪状态。

  • TimedWaiting:超时等待状态

超时等待与等待状态一样,唯一的区别就是多了超时机制,不会一直等待被其他线程主动唤醒,而是到达指定时间后会自动唤醒

以下函数会触发进入超时等待状态

  • wait(long)
  • join(long)
  • LockSupport.parkNanos(long)
  • LockSupport.parkUtil(long)
  • sleep(long)

其中wait(long)、join(long)函数会让JVM把线程放入锁等待队列。

  • Blocked:阻塞状态

运行状态的线程获取同步锁失败或发出I/O请求,该线程进入阻塞状态。

如果是获取同步锁失败J V M还会把该线程放入锁的同步队列。

同步锁被释放时,锁的同步队列会出队所有线程,进入就绪状态。

I/O处理完毕时,该线程重新回到就绪状态。

相关函数:

wait()、notify()等函数依赖于“同步锁”,定义在Object类中。wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!其他线程获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。

sleep() 定义在Thread.java中。
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

join() 定义在Thread.java中。
join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。

二、知识点详解

1.常用的实现多线程的2种方式:Thread 和 Runnable

Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:

public interface Runnable {
    public abstract void run();
}

Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。

Thread 是一个类。Thread本身就实现了Runnable接口。其作用也是实现多线程。它的声明如下:

public class Thread implements Runnable {}

Thread 和 Runnable 的区别: Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
通常,建议通过“Runnable”实现多线程!

2.start() 和 run()的区别:

start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
run()   : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

3.synchronized关键字

(1)原理:在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。

(2)基本规则:

第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块
第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。(因为锁只有一个啊!!)

(3)synchronized方法 和 synchronized代码块:

“synchronized方法”是用synchronized修饰方法,示例:

public synchronized void foo1() {
    System.out.println("synchronized methoed");
}
synchroniz

而 “synchronized代码块”则是用synchronized修饰代码块,示例:

public void foo2() {
    synchronized (this) {
        System.out.println("synchronized methoed");
    }
}

 synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。

synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。

(4)实例锁 和 全局锁

实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
               实例锁对应的就是synchronized关键字。
全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
               全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

1.进程和线程的区别,进程间如何通信
进程:系统运行的基本单位,进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。

线程:独立运行的最小单位,一个进程包含多个线程且它们共享同一进程内的系统资源

进程间的通信:通过管道、 共享内存、信号量机制、消息队列等。

2. 什么是线程上下文切换
当一个线程被剥夺cpu使用权时,切换到另外一个线程执行,通过中断来切换。

 3.什么是死锁
死锁指多个线程在执行过程中,因争夺资源造成的一种相互等待的僵局

 死锁的必要条件

  • 互斥条件:同一资源同时只能由一个线程读取
  • 不可抢占条件:不能强行剥夺线程占有的资源
  • 请求和保持条件:请求其他资源的同时对自己手中的资源保持不放
  • 循环等待条件:在相互等待资源的过程中,形成一个闭环

想要预防死锁,只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,还可以用银行家算法可以预防死锁 

4.Synchrpnized和lock的区别

Synchrpnized的作用:

  • 保证变量对所有线程的可见性。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。
  • 禁止指令重排序优化。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。

Synchrpnized底层原理:

Java 对象底层都关联一个的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者, monitor 在被释放前不能再被其他线程获取。

Synchronized关键词使用方法:

1. 直接修饰某个实例方法 2. 直接修饰某个静态方法 3. 修饰代码块

区别:

(1)synchronized是关键字,lock是一个类

(2) synchronized在发生异常时会自动释放锁,lock需要手动释放锁

(3)synchronized是可重入锁、非公平锁、不可中断锁,lock的ReentrantLock是可重入锁,可中断锁,可以是公平锁也可以是非公平锁

(4)synchronized是JVM层次通过监视器实现的,Lock是通过AQS实现的

5.什么是AQS锁?
AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。

AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。

AQS的原理是,AQS内部有三个核心组件,一个是state代表加锁状态初始值为0,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的:(因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点)

可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程。

常见的AQS锁:AQS分为独占锁和共享锁

ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队

Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。

应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票

CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。

应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行

CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值

应用场景:多线程计算数据,最后合并计算结果的应用场景

6.什么是CAS锁
CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是

(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作 

(2)CAS频繁失败导致CPU开销大

(3)ABA问题:线程1和线程2同时去修改一个变量,将值从A改为B,但线程1突然阻塞,此时线程2将A改为B,然后线程3又将B改成A,此时线程1将A又改为B,这个过程线程2是不知道的,这就是ABA问题,可以通过版本号或时间戳解决

7.Synchronized锁原理和优化
Synchronize是通过对象头的markwordk来表明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,成本很高,此时这种锁叫重量级锁,在JDK1.6以后引入了偏向锁、轻量级锁、重量级锁

偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁

轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过CAS方式获取锁,不会阻塞,提高性能,

重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低

注意,锁只能升级不能降级

8.sleep()和wait()的区别

 (1)wait()是Object的方法,sleep()是Thread类的方法

(2)wait()会释放锁,sleep()不会释放锁

(3)wait()要在同步方法或者同步代码块中执行,sleep()没有限制

(4)wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒

9.yield()和join()区别
yield()调用后线程进入就绪状态

A线程中调用B线程的join() ,则B执行完前A进入阻塞状态

10.线程池七大参数
核心线程数:线程池中的基本线程数量

最大线程数:当阻塞队列满了之后,逐一启动

最大线程的存活时间:当阻塞队列的任务执行完后,最大线长的回收时间

最大线程的存活时间单位

阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列

线程工厂:用于生产线程

任务拒绝策略:阻塞队列满后,拒绝任务,有四种策略(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争

11.Java内存模型 
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取

具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的

由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题

12.保证并发安全的三大特性?
 原子性:一次或多次操作在执行期间不被其他线程影响

可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道

有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排

 13.volatile
保证变量的可见性和有序性,不保证原子性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
单例模式双重校验锁变量为什么使用 volatile 修饰? 禁止 JVM 指令重排序,new Object()分为三个步骤:为实例对象分配内存,用构造器初始化成员变量,将实例对象引用指向分配的内存;实例对象在分配内存后实才不为null。如果分配内存后还未初始化就先将实例对象指向了内存,那么此时最外层的if会判断实例对象已经不等于null就直接将实例对象返回。而此时初始化还没有完成。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值