1、线程基础
1.1 创建线程的几种方式
- 继承Thread 类,覆盖run方法
- 实现Runable接口。实现run方法。然后 通过Thread类构造方法获取Thread对象。
- 实现Callable接口。实现call方法。 call方法可以抛出异常。也可以有返回值。
run与start 调用run方法任然是主线程在执行。调用start才会启动多线程
public class A {
static class MyCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "call";
}
}
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new MyCall());
Thread thread = new Thread(futureTask, "T1");
thread.start();
//主线程会阻塞,直到futureTask线程执行完获取到返回结果才会继续执行
String s = futureTask.get();
System.out.println(s);
}
}
1.2 Thread的几个常用方法
Thread.sleep(ms)
当前线程执行等待操作。xxx毫秒后继续执行。Thread.yield()
当前线程放弃当前时间片。(有可能下个时间片还是给到当前线程)t1.join()
当前线程等待。执行t1线程。直到t1线程执行完成再执行当前线程。wait notify ????
1.3 线程之间的状态转化
- 新建 刚new出来的线程出于这个状态
- ready 调用start方法,但是还未获取时间片
- running 调用start方法,且未获取时间片
- TimeWaiting 嫌弃等待 调用 sleep方法
- Waiting 无限期等待 调用join方法 wait方法
- block 阻塞 。等待锁标记
- Teminated 终止状态。该状态不可逆
1.4 synchronized 同步锁
- 对方法或者代码块进行加锁。加锁的方法或者代码块同一个时间段只允许一个线程访问
- 加在普通方法上 等价于对this进行加锁
- 加在静态方法上等价于对类对象进行加锁
synchronized并不能解决乱序问题
- 偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。- 轻量级
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。- 重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。- 异常锁:synchronized 锁代码块发生异常时会释放锁标记。
public class A {
/**
* 可重入锁验证
*/
public static void main(String[] args) throws Exception {
A a = new A();
Thread t1 = new Thread(() -> a.m1(), "t1");
Thread t2 = new Thread(() -> a.m1(), "t2");
t1.start();//m1,m2都是同一把锁。所以t1都可以访问
t2.start();//t2 一直在等待
}
synchronized void m1() {
while (true) {
System.out.println("m1----" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
}
m2();
}
}
synchronized void m2() {
System.out.println("m2----" + Thread.currentThread().getName());
}
}
1.5 volatile
- 保证线程可见性
堆空间中的成员变量是线程共享的。当线程要修改这个变量时需要先把值copy到自己的工作内存中(操作数栈)。待修改完成后再写回去。在这期间若变量的值被别的线程改变。就会影响数据的一致性。
添加volatile关键字时。jvm在编译时会通过 ACC_VOLATILE标记
jvm层面通过读写屏障 cpu层面通过MESI协议保证线程的可见性。
volatile并不能保证原子性
volatile内存区的读写 都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
- 单例模式的双重检查
public class AAA {
private static AAA a;
public static AAA getInstance() {
if (a == null) {
synchronized (AAA.class) {
if (a == null) {
a = new AAA();
}
}
}
return a;
}
}
以上代码虽然加了锁但是任然会存在问题。有可能AAA在刚完成初始化的时候就把引用指向了A。这个时候其他线程就可能得到未初始化的对象。因此要给属性a 加volatile 关键字。禁止指令重排序。
CAS锁,乐观锁
- jdk实现 AtomicXXXX 比如 AtomicInteger 通过compareandSet执行修改操作。先比较再赋值。
- cas实现原理。
cas的计算会有三个值 v Expected newValue
V就是操作变量的当前值 Expect是期望值即执行运算之前的值。 newValue就是执行运算之后的值。当前两个值相同时。写入新的值否则就要更新Expected的值然后进行重试。
- ABA问题
- 如果是基本类型无影响。
- 如果是引用类型。可以通过添加版本号来实现。每次修改version加一。
AtomicStampedReference
实现。- AQS:
ReentrantLock
同synchronized一样都是可重入锁。,他的底层是CAS锁。
- 与synchronized区别:
- synchronized发生异常以后会自动释放锁。reentrantLock不会。需要手动释放。
- ReentrantLock可以使用tryLockf方法。无论是否取得锁不影响后续操作。
- 可打断。加锁时选择lockInterruptibly方法加锁。可以使用interrupt方法打断其他线程强制占有锁
- 公平锁 (ReenttrantLock 默认是不公平锁,所有等待的线程一起抢锁。没有先来后到)
ReentrantLock lock=new ReentrantLock(true)
开启公平锁。谁先来谁先拿到锁。
CountDownLatch
相当于一个同步器。在创建CountDownLatch的时候指定线程数量。每个线程完成的时候调用一下
countDown
方法。然后可以使用await()方法
阻塞线程。等所有线程结束后才继续执行。
public static void main(String[] args) throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(100);
Thread [] threads=new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i]=new Thread(()->{
System.out.println(Thread.currentThread().getName()+" is running");
countDownLatch.countDown();
},"thread-"+i);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
countDownLatch.await();//线程会在该地方阻塞。直到所有线程执行完毕才会打印over
System.out.println("over");
}
CyclicBarrier
线程栅栏。定义时会指定围栏大小。如果线程执行过程中调用了await方法。该线程就会等待直到等待线程达到栅栏指定的阈值
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierTest {
public static void main(String[] args) {
//指定等待线程的大小。如果没有那么多的线程。那么将永远阻塞
CyclicBarrier barrier = new CyclicBarrier(100, () -> {
System.out.println(Thread.currentThread().getName());
System.out.println("发车了");//该代码由到达阈值的线程执行。
});
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"is running");
TimeUnit.SECONDS.sleep(2);
barrier.await();//等运行的线程数到达阈值才会继续执行
System.out.println(Thread.currentThread().getName()+"is running");//等队列满了才会执行此代码
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},"thread-"+i).start();
}
}
}
LockSuport
# AQS队列(AbstractQueuedSynchronizer)
>- AQS队列又被称为CLH队列它由一个状态值 `volatile int state`和一个`双向链表`组成。state由多个线程抢占。谁拿到这个status就表示谁抢占到这把锁。AQS的核心就是一个共享数据,一堆线程去相互抢夺占用。这个就是AQS
>- 在reentrantLock中当`getStatus()`为1时表示锁已经被占用。0表示没有被占用。
>- 双向表中的node节点存储的是要抢占节点的线程。
>- 当发现status为1时会加入队列中(`addWaiter()`)等待锁释放以后再去抢占锁。
>- 当获取到status为0时,用cas锁尝试将status值设置为1.如果成功掉用setExclusiveOwnerThread.吧当前线程设置为独占这把锁的状态。
# ThreadLocal
>多个线程访问ThreadLocal中的变量。只能获取到他自己set的数据。
## 1、ThreadLocal源码解析
```java
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程获取对应map 这里的map对象对应 Thread.threadLocals
//它是一个Map<ThreadLocal,<T>>其中 T是要存储的数据。
//一个线程可以绑定多个ThreadLocal对象。
ThreadLocalMap map = getMap(t);
if (map != null)
//key为threadLocal对象,value为存储的值
//如果之前set过会被覆盖
map.set(this, value);
else
//没有则创建一个新的map
createMap(t, value);
}
//很简单一目了然
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//没有就新创建一个
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取 t.threadLocals
ThreadLocalMap map = getMap(t);
//如果之前设置过 t.threadLocals应该不为空
if (map != null) {
//t.threadLocals的格式 Map<ThreadLocal,<T>>
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//返回默认值具体默认值由子类的initialValue方法实现。可自定义
return setInitialValue();
}
java的四种引用
- 一般引用(强引用)
A a=new A();
a是一个强引用。它指向的对象不会被回收。 - 软引用
SoftRefence<T> m=new SoftRefence<T>()
内存不够时会被回收。非常适合做页面缓存
- 弱引用
WeakRefence<T> m= new WeakRefence<T>();
只要遇到gc就会被回收 - 虚引用 jvm内部使用