多线程
1、实现多线程的方式
1. 通过Thread类
public class A_通过Thread实现 {
public static void main(String[] args) {
/*
* 1 继承Thread
* 2 实现类去重写Thread中的抽象方法:run()
* 3 创建实现类的对象,开启线程
*/
MyThread th1 = new MyThread();
MyThread th2 = new MyThread();
th1.start();
th2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
}
2. 通过Runnable接口
public class B_通过Runnable方法实现 {
public static void main(String[] args) {
/*
* 1 自己造一个类,实现Runnable接口
* 2 实现类去实现Runnable中的抽象方法:run()
* 3 创建实现类的对象
* 4 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5 通过Thread类的对象调用start()
*/
MyRunnable myRunnable = new MyRunnable();
Thread th = new Thread(myRunnable);
th.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
3. 利用Callable接口和Future接口
public class C_通过Callable和Future实现 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 1 实现Callable接口
* 2 实现call()方法
* 3 创建MyCallable实体
* 4 创建FutureTask实体
* 5 创建线程
* 6 开启线程
* 7 调用get()方法,获取结果
*/
MyCallable call = new MyCallable();
FutureTask<Integer> fu = new FutureTask<>(call);
Thread th = new Thread(fu);
th.start();
Integer result = fu.get();
System.out.println(result);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//计算1~100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
2、线程的常见方法
-
String getName()
- 获取线程的名称
-
void setName(Stirng name)
- 设置线程的名称
-
static Thread currentThread()
- 获取当前的线程对象
-
static void sleep(long time)
- 让线程休眠指定的时间,单位为毫秒
-
setPriority(int newPriority)
- 设置线程的优先级
- 线程的调度
- 抢占式调度:每个线程随机执行,执行的时间和顺序是随机的 — java 采用抢占式的调度方法
- 非抢占式调度:每个线程轮流执行,而且每个线程执行的时间也基本是一样的
- Java 中,优先级越高,抢占线程的概率越大
-
final int getPriority()
- 获取线程的优先级
- Java 中默认的有优先级是 5
-
final void setDaemon(boolean on)
- 设置为守护线程
- 当其他的非守护线程执行完毕后,守护线程就会陆续结束
- 通俗:当女神线程结束了,那末备胎线程也没有存在的必要了
-
public static void yield()
- 出让线程
- 出让当前线程所抢占的CPU,让线程重新抢占CPU
-
public static void join()
-
插入线程
-
表示把
t
线程,插入到当前线程之前。MyThread t = new MyThread(); t.start(); t.join(); //表示把这个线程,插入到main线程之前,即:等t线程执行完毕后,再执行main线程
-
3、线程的生命周期
4、synchronized用法
1. 同步代码块
public class synchronizedTest {
public static void main(String[] args) {
MyThread th1 = new MyThread();
MyThread th2 = new MyThread();
MyThread th3 = new MyThread();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
class MyThread extends Thread{
//表示这个类的所有对象都共享ticket数据
static int ticket = 0;
//锁对象,一定是唯一的
static Object obj = new Object();
//一般用,当前类的Class对象
@Override
public void run() {
while(true){
//同步锁
synchronized (MyThread.class){
if(ticket < 100){
try {
Thread.sleep(100);
} catch (Exception e){
e.printStackTrace();
}
ticket ++;
System.out.println(getName() + "卖出了第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
2. 同步方法
- 就是把
synchronized
关键字加到方法上- **格式:**修饰符
synchronized
返回值类型 方法名 (参数列表){…} - 特点:
- 同步方法是锁住方法里面所有的代码
- 锁对象不能自己指定
- 非静态方法的锁对象是:
this
- 静态方法的锁对象是:
当前类的字节码文件
- 非静态方法的锁对象是:
- **格式:**修饰符
3. Lock锁
public class LockTest {
public static void main(String[] args) {
MyThread2 th1 = new MyThread2();
MyThread2 th2 = new MyThread2();
MyThread2 th3 = new MyThread2();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
class MyThread2 extends Thread {
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
try {
if(ticket == 100) {
break;
} else {
Thread.sleep(10);
ticket ++;
System.out.println(getName() + "买了第" + ticket + "张票!!!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
5、多线程的等待唤醒机制
1. 生产者和消费者
public class Test {
public static void main(String[] args) {
Cook cook = new Cook();
Foodie foodie = new Foodie();
cook.start();
foodie.start();
}
}
class Desk {
// 0:表示没有食物,1:表示有食物
public static int foodFloat = 0;
//锁对象
public static Object lock = new Object();
//消费的数量
public static int count = 10;
}
class Foodie extends Thread {
@Override
public void run() {
//死循环
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
} else {
if(Desk.foodFloat == 0){
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
Desk.count --;
System.out.println("吃货还能吃" + Desk.count + "碗饭");
Desk.foodFloat = 0;
Desk.lock.notifyAll();
}
}
}
}
}
}
class Cook extends Thread {
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
} else {
if(Desk.foodFloat == 1){
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
System.out.println("厨师开始做食物");
Desk.foodFloat = 1;
Desk.lock.notifyAll();
}
}
}
}
}
}
2. 阻塞队列
public class Test {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
Cook cook = new Cook(queue);
Foodie foodie = new Foodie(queue);
cook.start();
foodie.start();
}
}
class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
try {
queue.put("面条");
System.out.println("厨师开始做面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
try {
String s = queue.take();
System.out.println("吃" + s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
6、线程的状态
7、多线程的可见性
每个线程去使用共享内存的时候,是把共享内存的内容复制一份副本进入自己的内存。
1:第一次访问这个共享内存的时候,加载进入副本,只要不修改 就一直使用这个副本,不会重新去拿。2:当修改了副本之后,他会立刻同步到共享区域中,后续使用的就是修改后的这副本。
8、多线程的指令重排
Java
会把有关联的代码放在一起,提高CUP的执行效率。在多线程模式下,可能导致这个线程还没有修改变量的值,另一个线程就获取到旧的值进行判断。
9、原子性与原子类
当两个线程同时修改同一个共性资源时(static修饰的),可能导致有些的修改不起作用。
例如:线程1,将 a 自增 100次,线程2,将 a 自增100次,可能最终只是自增了不到200次
因此,原子性就是指,把 a++的三步合成一步,只有三步全部执行完成了才会去释放CUP的执行权。
解决方法:
- 加锁,即:将a++加一个锁,保证这个代码块执行完成后,才会释放CPU的执行权
原子类:保证数据修改的原子性,即:上述结果的三步合成一步
int - AtomicInteger
long - AtomicLong
boolean - AtomicBoolean
引用数据类型 - AtomicReference
原子类源码:
11、volatile的使用
- **使用范围:**只能修饰堆内存或方法区中的变量,即:
static
修饰或成员变量。 - 作用:
- 使被修饰的变量具有可见性,即:当多个线程同时获取一个变量时,每次都会去主内存中获取这个变量,而不是从自己栈帧中的副本中获取。
- 阻止代码重排
12、ThreadLocal
-
作用:提供线程内的局部变量,不同的线程之间不会互相干扰,这种变量在线程的生命周期内起作用,减少同一线程内多个函数或组件之间一些公共变量传递的复杂度。
-
线程并发:在线程并发的场景下
-
传递数据:我们可以通过ThreadLoacl在同一线程,不同组件中传递公共变量
-
线程隔离:每个线程的变量都是独立的,不会互相影响
/* 线程隔离: 在多线程的场景下,每个线程中的变量都是相互独立的 线程A:设置(变量1) 获取(变量1) 线程B:设置(变量2) 获取(变量2) ThreadLocal: 1. set():将变量绑定到当前线程中 (在别的线程中获取不到) 2. get():获取当前线程绑定的变量 */
-
-
用法
13、AQS
1、什么是AQS?
ASQ本质就是JUC包下的一个抽象类,AbstractQueuedSynchronizer
类,它是一个基础类,本身并没有实现什么具体的并发共功能,但是很多
JUC包下的工具都是基于AQS实现的。
2、AQS的核心内容是什么?
AQS内部有三个核心内容,一个属性,两个结构。
第一个核心属性:state
,就拿ReentrantLock
来说,比如 state 为0,代表当前没有线程持有这个lock锁
private volatile int state;
第二个核心结构:一个同步队列(双向链表):拿ReentrantLock
来说,如果某个线程想获取锁资源,但发现这个锁资源已经被占用了,那末,这个线程就会被封装成Node
对象,进入同步队列的尾部,排队,并挂起等待锁资源。
private transient volatile Node head;
private transient volatile Node tail;
第三个核心结构:Condition的单线链表:当持有lock锁的线程,执行了await
方法,会将当前线程封装为Node,插入到单向链表中,等待被其他线程执行signal
方法唤醒,然后扔到同步队列中等待竞争锁资源。
本质就是AQS内部类,ConditionObject
中提供的一个基于Node对象组成的单线链表。
private transient Node firstWaiter;
private transient Node lastWaiter;
该链表的作用是,存放等待被唤醒的线程。
3、Lock锁和AQS的继承关系?
以ReentrantLock
锁为例
整个ReentrantLock获取锁的逻辑:
CSA解释:CAS就是一个CPU支持的一个原语,在经过编译后,CAS指令会被编译成cmpxchg,这个就是CPU支持的原语,CAS本质就是Compare And Swap
4、公平锁和非公平锁的直观体现?
从源码的层面来看,公平锁与非公平锁只有一点点的不同。
lock 方法一般是获取锁资源的入门方法。
通过源码可以看到,非公平锁会直接尝试抢一次锁资源。
//非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁
final void lock() {
acquire(1);
}
5、AQS为acquire方法?
acquire
方法就是前面的核心处理逻辑。
1、先走tryAcquire
方法,再次尝试强锁,抢到了,走人。
2、没抢到走addWaiter
方法,准备排队,将线程封装为Node对象,添加到AQS的同步队列中。
3、再走acquireQueued
方法,再次获取锁,还是挂起线程,就要看方法内部的具体逻辑。
//acquire 方法实现
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//方便观察
public final void acquire(int arg) {
if (!tryAcquire(arg)){
//拿锁失败
Node node = addWaiter(Node.EXCLUSIVE);
acquireQueued(node, arg))
selfInterrupt();
}
}
6、AQS的tryAcquire底层逻辑?
tryAcquire
有两种实现,公平锁和非公平锁。
// 非公平锁的 tryAcquire 的实现逻辑 (java 8)
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
//获取state属性的值
int c = getState();
if (c == 0) {
// 当前锁资源没有线程持有。执行CAS,尝试将state从0改为1
if (compareAndSetState(0, acquires)) {
// 修改成功,将 exclusiveOwnerThread 设置为当前线程
setExclusiveOwnerThread(current);
// 返回true,拿锁成功
return true;
}
}
// 当前锁资源被占用,占用锁资源的是不是我自己。。。。
else if (current == getExclusiveOwnerThread()) {
// 所重入的逻辑,直接对state + 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 赋值给state
setState(nextc);
// 锁重入成功
return true;
}
// 那锁失败
return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors() 如果当前锁资源没有被占用,需要通过一定的判断才可以尝试抢锁。
// 1、如果AQS的同步队列没有排队的Node,直接抢锁!执行CAS!
// 2、如果有排队的Node,并且排在第一名的是当前线程,直接抢锁,执行CAS!
// 3、如果有排队的Node,并且排在第一名的不是当前线程,不能抢锁,直接返回false
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
7、AQS的addWaiter的底层逻辑?
addWaiter 操作中,只需要关注一个事情,如果出现了并发插入到AQS同步队列这个操作。
需要保证原子性。而保证原子性的方式,就是哪个线程成功的基于CAS将tail的指向换成了自己,谁就优先插入到同步队列的末尾。
如果CAS失败,会走到一个死循环,不断重试1,2操作
8、AQS的acquireQueued底层逻辑
final boolean acquireQueued(final Node node, int arg) {
// 这里省略了取消节点和中断相关的信息
for (;;) {
// 获取当前 Node 节点的 prev 节点(上一个节点)
final Node p = node.predecessor();
// p == head,说明当前节点就是head.next,排在第一名的节点
if (p == head) {
//直接走 tryAcquire 抢锁。
if(tryAcquire(arg)){
// 到这,说明抢锁成功!
// 抢锁成功后,做换头操作。
// 会将获取资源的 node,作为新的head节点,并且将prev以及之前head的next置为null,目的是为了让之前的head可以被快速回收
// 换头是为了保留状态......
setHead(node);
p.next = null;
return false;
}
}
// 不是第一名,不让抢。第一名抢锁失败。
// 挂起逻辑
// Node 节点的状态
// CANCELLED == 1:当前Node不排了,马上要走了
// SIGNAL == -1:当前Node的next挂起了。
// 其他状态 == 0...正常状态
if (shouldParkAfterFailedAcquire(p, node)){
// 挂起线程
parkAndCheckInterrupt();
}
}
}
// pred:上一个节点。 node:当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点状态
int ws = pred.waitStatus;
// 上一个节点状态是-1,直接返回true
if (ws == Node.SIGNAL)
return true;
// 如果上一个节点状态是取消,基于do-while循环往前找,直到找到一个状态不是取消的节点。
// 绕过这些取消的节点。
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果上一个节点状态正常,直接基于CAS,将上一个节点装填修改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
14、锁
1、ReentrantLock可重入锁
可重入锁分为两种:公平锁、非公平锁
2、ReentrantReadWriteLock读写锁
例如:
悲观锁:效率低,不支持并发
乐观锁:效率高,支持并发
特点:一个资源可以有多个线程读取,但是只能由一个线程写入
锁降级:将读锁 降级到 读锁(过程:获取写锁 - 获取读锁 - 释放写锁 - 释放读锁)
读锁不能升级为写锁
3、阻塞队队列
BlockingQueue的方法