8 minute read

基本概念

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

进程调度算法(CPU 调度算法)

进程调度算法

当CPU空闲的时候,操作系统会选择某个空闲处于就绪状态的进程,为其分配CPU资源。

  • 先来先服务调度算法。每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
  • 最短作业优先调度算法。优先选择运行时间最短的进程来运行。
  • 高响应比优先调度算法。每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行。响应比 =(等待时间+要求服务时间)/要求服务时间。
  • 时间片轮转调度算法。每个进程被分配一个时间段,允许该进程在该时间段中运行。
  • 最高优先级调度算法。从就绪队列中选择最高优先级的进程运行。
  • 多级反馈队列调度算法。「时间片轮转算法」和「最高优先级算法」的综合。

线程

  • 一个进程之内可以分为一到多个线程
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

两者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。进程拥有共享的资源,如内存空间等,供其内部的线程共享。
  • 进程间通信较为复杂,同一台计算机的进程通信称为IPC,不同计算机之间的进程通信需要通过网络。
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。线程更轻量,线程上下文切换成本一般要比进程上下文切换低

并发与并行

  • 并发(Concurrent)是一个CPU在不同的时间去不同线程中执行指令
  • 并行(Parallel)是多个CPU同时处理不同线程

同步与异步

  • 同步指需要等结果返回才能继续运行
  • 异步指不需要等结果返回

举例:读取文件的,由于IO操作需要等文件全部读取结束才能继续运行,所以效率低下,才有了后来的NIO(非阻塞IO)

死锁

t1线程获得锁A,同时还想获得锁B。t2线程获得锁B,同时还想获得锁A。两者都进行不下去的情形称为死锁。

定位死锁工具,jconsole或者jps,jstack

public static void main(String[] args) {
		final Object A = new Object();
		final Object B = new Object();
		new Thread(()->{
			synchronized (A) {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (B) {

				}
			}
		}).start();

		new Thread(()->{
			synchronized (B) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (A) {

				}
			}
		}).start();
	}

死锁四个条件

  • 互斥条件。该资源任意一个时刻只由一个线程占有
  • 请求并持有条件。一个线程因请求资源而阻塞时,对已获得的资源保护不放
  • 不可剥夺条件。线程获取到的资源在未用完前不能被其他线程抢占,只由自己用完后才释放
  • 环路等待条件。若干线程之间形成的头尾循环的等待资源关系。

破坏死锁

  • 破坏请求并持有条件
  • 破坏环路等待条件

避免死锁

有序申请资源(银行家算法等)

活锁

两个线程互相改变对方的结束条件,谁也没法结束。

解决方法,在线程执行时,给予不同的间隔时间

饥饿

因为优先级太低,一直得不到资源。或者在顺序加锁时,也会出现。

主线程与守护线程

守护(daemon)线程:当其他全部非守护线程结束时,守护线程就结束。只要有一个用户线程还没结束,正常情况下JVM就不会退出。举例:垃圾收集器线程就是一种守护线程。

并发编程三个重要特性

  1. 原子性。一个或多次操作,要么所有的操作全部都执行,要不都不执行,且不会受到任何因素的干扰而中断。synchronized可以确保原子性
  2. 可见性。当一个线程对共享变量进行了修改,那么其他线程都应立即能看到修改后的最新值。volatile可以确保可见性。
  3. 有序性。代码在执行的过程中的先后顺序,Java编译器会进行优化,即指令重排。volatile可以禁止指令重排,确保有序性。

Java线程

线程创建与运行

方法一,使用Thread

缺点:线程和任务合并在了一起,没有返回值

        Thread thread = new Thread(() -> System.out.println(111));
        thread.start();

方法二,使用Runnable配合Thread(推荐)

线程和任务分开,缺点:没有返回值

	   Runnable runnable = () -> System.out.println(111);
       Thread thread = new Thread(runnable);
       thread.start();

方法三,FutureTask配合Thread

优点:可以拿到返回值

        FutureTask<String> futureTask = new FutureTask<>(() -> "hello");
        new Thread(futureTask).start();
        System.out.println(futureTask.get());//hello

线程运行原理

结合之前学习的JVM,JVM为每个线程分配虚拟机栈、程序计数器、本地方法栈。每个方法被执行的时候都会创建一个栈帧,存储在虚拟机栈中,每个线程都会维护自己的栈帧。

线程上下文切换(Thread Context Switch):任务从保存到再加载的过程就是一次上下文切换

因为一些原因进行线程切换,程序计数器会记住下一条JVM指令的执行地址。

被动原因:

  • 线程CPU时间片用完
  • GC
  • 有更高优先级的线程需要运行

主动原因:

线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

Thread常见方法

方法名 static 功能说明 注意
start()   启动一个新线程 只是让线程进入就绪状态,代码不一定立刻运行。start只能调用一次,调用多次会出现IllegalThreadStateException
wait()   线程被阻塞挂起 如果调用线程没有获取锁,则调用时会抛出IllegalMonitorStateException
run()   新线程启动时会调用的方法 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作
join()   等待线程运行结束  
join(long n)   等待线程运行结束,最多等待n毫秒  
getId()   获取线程唯一长整型id  
getName()   获取线程名  
setName(String)   修改线程名  
getPriority()   获取线程优先级  
setPriority(int)   修改线程优先级 Java中规定优先级为1-10的整数,较大优先级能提高被CPU调度几率
getState()   获取线程状态 Java中用枚举表示,NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
isInterrupted()   判断是否被打断 不会清除打断标记
isAlive()   线程是否存活(还没有运行完毕)  
interrupt()   打断线程 如果被打断的线程正在sleep,wait,join则会导致interruptedException,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记,park的线程被打断,也会设置打断标记
interrupted() static 判断当前线程是否被打断 会清除打断标记(设置为false)
currentThread() static 获取当前正在执行的线程  
sleep(long n) static 让当前执行的线程休眠n毫秒  
yield() static 提示线程调度器让出当前线程对CPU的使用  

start与run

start是让创建线程进入就绪状态,run是直接调用方法,线程还是处于新建状态。

调用start()方法才可启动线程并使线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行

可以看到直接用run,还是main线程在执行

    Thread thread = new Thread(() -> {
        System.out.println(Thread.currentThread()+"hello");//Thread[main,5,main]hello
    });
    thread.run();

sleep与yield

  • sleep让线程阻塞。状态由Running变为Time Waiting。可用interrupt打断正在睡眠的线程,这时会抛interruptedException。推荐用TimeUnit替代,更好的可读性。
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

应用:防止CPU占用100%

某些时候可能要写死循环,如果不加sleep,CPU占用率会过高。

while(true){
    try{
        Thread.sleep(50);
    }catch(InterruptedException e){
        e.printStackTrace();
    }
    //do something
}
  • yield让出当前线程。状态由Running变为Runnable

join

    public static void main(String[] args) {
        Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
                System.out.println("hello");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        //main线程等thread线程执行结束
        thread.join();
    }

interrupt

  • 如果一个线程在运行中被打断,打断标记会被置为true
  • 如果打断因sleep、wait、join方法而被阻塞的线程, 打断标记会被置为false
    Thread thread=new Thread(()->{
        System.out.println("开始");
        //暂停
        LockSupport.park();
        System.out.println("继续");
        System.out.println(Thread.currentThread().isInterrupted());//true
    });
    thread.start();
    try {
        Thread.sleep(1000);
        thread.interrupt();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

线程状态

Java线程状态有6种,NEW、RUNNABLE、TERMINATED、TIMED_WAITING、WAITING、BLOCKED

public class Test {
    public static void main(String[] args) {
        // NEW
        Thread t1 = new Thread(() -> {
            System.out.println("NEW 状态");
        }, "t1");

        // RUNNABLE
        Thread t2 = new Thread(() -> {
            while (true) {

            }
        }, "t2");

        t2.start();

        // TERMINATED
        Thread t3 = new Thread(() -> {
            System.out.println("running");
        }, "t3");
        t3.start();

        // TIMED_WAITING
        Thread t4 = new Thread(() -> {
            synchronized (Test.class) {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        t4.start();

        // WAITING
        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t5");
        t5.start();

        Thread t6 = new Thread(() -> {
            synchronized (Test.class) {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t6");
        t6.start();

        // 主线程休眠 1 秒, 目的是为了等待 t3 线程执行完
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("t1 线程状态: {}" + t1.getState());//NEW
        System.out.println("t2 线程状态: {}" + t2.getState());//RUNNABLE
        System.out.println("t3 线程状态: {}" + t3.getState());//TERMINATED
        System.out.println("t4 线程状态: {}" + t4.getState());//TIMED_WAITING
        System.out.println("t5 线程状态: {}" + t5.getState());//WAITING
        System.out.println("t6 线程状态: {}" + t6.getState());//BLOCKED
    }
}

共享模型之管程

一段代码块如果存在对共享资源的多线程读写操作,称这段代码块为临界区

多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称之为发生了竞态条件

为避免临界区的竞态条件发生,有多种解决方案:

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized

俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程想获得时会被阻塞,这样就能保证持有锁的线程可以安全的执行临界区内的代码。用对象锁保证了临界区内代码的原子性。不能用synchronized修饰构造方法(构造方法本身就是线程安全的)。可重入锁,非公平锁。

class Test{
    public synchronized void test(){
        
    }
}
//等价于
class Test{
    public void test(){
        synchronized(this){
            
        }
    }
}
class Test{
    public synchronized static void test(){
        
    }
}
//等价于
class Test{
    public static void test(){
        synchronized(Test.class){
            
        }
    }
}

synchronized底层原理

每个Java对象都可以关联一个monitor,如果用synchronized给对象上锁,则该对象的对象头中的Mark word中被设置为指向monitor对象的指针。monitor中的owner指向该对象,后来的对象则是monitor中的entryList指向。

QQ截图20210508081623.png

synchronized优化原理(了解)

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

偏向锁

轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。

Java6中引入偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

锁消除

变量线程安全分析

成员变量和静态变量是否线程安全?

  • 如果没有被共享,则线程安全
  • 如果被共享了,如果只有读,那么安全。如果有读写,则不安全。

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用范围,则是线程安全的
    • 如果该对象逃离方法的作用范围,则线程不安全。(逃逸分析)

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector(List的线程安全实现类)
  • Hashtable(Hash的线程安全实现类)
  • java.util.concurrent包下的类

但是方法组合不是原子的,线程不安全。

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

示例一:

MyAspect切面类只有一个实例,成员变量start 会被多个线程同时进行读写操作

@Aspect
@Component
public class MyAspect {
    // 是否安全?不安全
    private long start = 0L;
    
    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }
    
    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}

示例二:

public class MyServlet extends HttpServlet {
 	// 是否安全?安全
 	private UserService userService = new UserServiceImpl();

 	public void doGet(HttpServletRequest request, HttpServletResponse response) {
 		userService.update(...);
 	}
}
public class UserServiceImpl implements UserService {
     // 是否安全?安全
     private UserDao userDao = new UserDaoImpl();
     public void update() {
     	userDao.update();
     }
}
public class UserDaoImpl implements UserDao {
     public void update() {
         String sql = "update user set password = ? where username = ?";
         	// 是否安全?安全
             try (Connection conn = DriverManager.getConnection("","","")){
             // ...
             } catch (Exception e) {
             // ...
         }
     }
}

示例三:

可以被子类继承重写,导致线程不安全

public abstract class Test {
    public void bar() {
        // 是否安全?不安全
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    public abstract foo(SimpleDateFormat sdf);
    public static void main(String[] args) {
        new Test().bar();
    }
}
    public void foo(SimpleDateFormat sdf) {
        String dateStr = "1999-10-11 00:00:00";
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    sdf.parse(dateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

wait/notify

必须获得对象锁才能调用这两个方法。

    ReentrantLock reentrantLock = new ReentrantLock();
    Thread thread = new Thread(()->{
        reentrantLock.lock();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    });
    thread.start();
    thread.wait();//java.lang.IllegalMonitorStateException

notify随机唤醒一个wait线程,notifyAll唤醒全部。

sleep(long n)和wait(long n)区别

  • sleep是Thread方法,wait是Object的方法
  • sleep不需要强制配合synchronized,wait需要
  • sleep()睡眠的同时不释放锁wait()释放锁

防止虚假唤醒(一个线程可以从挂起状态变为可运行状态,即使该线程没有被其他线程调用notify、notifyAll,或者被中断,或者等待超时。),wait的正确使用方式:

synchronized (lock) {
	while(//不满足条件,一直等待,避免虚假唤醒) {
		lock.wait();
	}
	//满足条件后再运行
}

synchronized (lock) {
	//唤醒所有等待线程
	lock.notifyAll();
}

park/unpark

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark;

与wait,notify比较:

  • unpark不需要配合Object使用
  • unpark可以唤醒指定线程,notify随机唤醒一个等待线程
  • unpark可以先unpark,notify不能先notify(需要在wait之后)

线程状态转换

  • 情况一:NEW –> RUNNABLE 当调用了 t.start() 方法时,由 NEW –> RUNNABLE
  • 情况二: RUNNABLE <–> WAITING 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争 竞争锁成功,t 线程从 WAITING –> RUNNABLE 竞争锁失败,t 线程从 WAITING –> BLOCKED
  • 情况三:RUNNABLE <–> WAITING 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING 注意是当前线程在 t 线程对象的监视器上等待 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
  • 情况四: RUNNABLE <–> WAITING 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
  • 情况五: RUNNABLE <–> TIMED_WAITING t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
  • 情况六:RUNNABLE <–> TIMED_WAITING 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING 注意是当前线程在 t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
  • 情况七:RUNNABLE <–> TIMED_WAITING 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
  • 情况八:RUNNABLE <–> TIMED_WAITING 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
  • 情况九:RUNNABLE <–> BLOCKED t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
  • 情况十: RUNNABLE <–> TERMINATED 当前线程所有代码运行完毕,进入 TERMINATED

ReentrantLock

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持可重入

基本语法

    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {
        reentrantLock.lock();
        try {

        }finally {
            reentrantLock.unlock();
        }
    }

可重入

如果同一个线程首次获得了这把锁,因为是锁的拥有者,因此有权利再次获得这把锁。

可中断

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //使用可中断方式获取锁,可以避免死锁
                    reentrantLock.lockInterruptibly();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("被打断,未能获取到锁");
                    return;
                }
                try {
                    System.out.println("获取到锁");
                } finally {
                    reentrantLock.unlock();
                }
            }
        });
        reentrantLock.lock();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();

锁超时

tryLock进行有限时间等待,如果未能获取到锁,就返回false

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                //等待有限时间
                if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("未能获取到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("未能获取到锁");
                return;
            }
            try {
                System.out.println("获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }
    });
    reentrantLock.lock();
    thread.start();

非公平锁

ReentrantLock默认是不公平的。可以通过构造函数开启公平锁(先等待的线程先获得锁),但一般没有必要开启,会降低并发度。

条件变量

  • await 前需要获得锁
  • await 执行后,会释放锁,进入condition等待
  • await的线程被唤醒后去重新竞争lock锁
  • 竞争lock锁成功后,从await后继续执行
    //创建新条件变量
    Condition condition1 = reentrantLock.newCondition();
    Condition condition2 = reentrantLock.newCondition();
    
    reentrantLock.lock();
    //进入休息室等待
    condition1.await();
    //唤醒休息室中的随机一个线程
    condition1.signal();
    //唤醒全部
    condition1.signalAll();

ThreadLocal

ThreadLocal是线程专属的本地变量,存储每个线程私有数据。

示例:

class Test implements Runnable {
    //SimpleDateFormat线程不安全,每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(test, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }

    }

    @Override
    public void run() {
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter =" + formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        formatter.set(new SimpleDateFormat());
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " formatter =" + formatter.get().toPattern());
    }
}

当改变线程自己的结果时,其他线程的结果不受影响,可见ThreadLocal的值是线程私有的。

synchronized与threadlocal区别

  synchronized threadlocal
原理 以时间换空间,只提供一份变量,让不同线程排队访问 以空间换时间,为每个线程提供了一份变量副本,从而实现同时访问而互不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

原理:

通过阅读源码,可以发现最终变量时存在ThreadLocalMap中,ThreadLocalMap存储以ThreadLocal为key,Object为value的键值对。

内存泄漏问题:

threadLocal使用的key为弱引用,垃圾回收时,key被清理掉,value不会被清理掉,使用完之后最好手动调用remove()方法。

可见性问题

因为run值是存储在主内存中的,每次读取都比较耗费资源,所以JIT对此做了优化,把值存储到了自己工作内存的高速缓存中,当main线程改变run值时,线程中的run值并没改变。

public class Demo {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {

            }
        }).start();

        Thread.sleep(1000);
        System.out.println("开始停止");
        run = false;//未能停止
    }
}

解决方法:volatile关键词。修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

public class Demo {
    volatile static boolean stop = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (stop) {

            }
        }).start();

        Thread.sleep(1000);
        System.out.println("开始停止");
        stop = false;//停止
    }
}

volatile原理

底层实现原理是内存屏障

  • 对volatile变量的写指令后会加入写屏障。写屏障保证在该屏障之前对共享变量的改动,都同步到主存当中。
  • 对volatile变量的读指令前会加入读屏障。读屏障保证在该屏障之后对共享变量的读取,加载的是主存中最新数据。

happens-before

一套可见性与有序性的规则总结

  • 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
static int x;
static Object m = new Object();

new Thread(()->{
    synchronized(m){
        x=10;
    }
}).start();

new Thread(()->{
    synchronized(m){
        System.out.println(x);
    }
}).start();
  • 线程对volatile变量的写,对接下来其他线程对该变量的读可见
volatile static int x;

new Thread(()->{
    x=10;
}).start();

new Thread(()->{
    System.out.println(x);
}).start();
  • 线程start前对变量的写,对该线程开始后对该变量的读可见
static int x;
x=10;
new Thread(()->{
    System.out.println(x);
}).start();
  • 线程结束前对变量的写,对其他线程得知它结束后的读可见
static int x;
Thread t1=new Thread(()->{
    x=10;
}).start();
t1.join();
System.out.println(x);
  • 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见
static int x;

public static void main(String[] args){
    Thread t2=new Thread(()->{
        while(true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println(x);
                break;
            }
        }
    });
    t2.start();
    new Thread(()->{
        sleep(1);
        x=10;
        t2.interrupt();
    }).start();
    
    while(!t2.isInterrupted()){
        Thread.yield();
    }
    System.out.println(x);
}
  • 对变量默认值的写,其他线程对该变量读可见

共享模型之无锁

compareAndSet简称CAS,底层是CPU级别的原子操作,所以不需要加锁

CAS特点

CAS适合线程数少、多核CPU的场景。

  • CAS是基于乐观锁的思想,不怕别的线程来修改共享变量,就算改了再重试
  • synchronized基于悲观锁,我上锁了你们都别想改
  • CAS体现的是无锁开发、无阻塞并发
    • 因为没有使用synchronized,所以线程不会陷入阻塞

原子整数

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
        AtomicInteger i = new AtomicInteger(10);

        System.out.println(i.getAndIncrement());//10
        System.out.println(i.incrementAndGet());//12
        System.out.println(i.getAndAdd(10));//12
        System.out.println(i.get());//22

        System.out.println(i.updateAndGet(x -> x * 10));//220

原子引用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
       //无法察觉ABA问题,即从A变为B再变为A的情况。 
	   AtomicReference<String> atomicReference=new AtomicReference<>("abc");

        atomicReference.compareAndSet("abc","sss");
        System.out.println(atomicReference.get());//sss

解决ABA问题,每次使用获取时间戳,如果时间戳不对则不修改

        AtomicStampedReference<String> atomicReference=new AtomicStampedReference<>("abc",0);

        int stamp = atomicReference.getStamp();
        atomicReference.compareAndSet("abc","sss",stamp,stamp+1);
        System.out.println(atomicReference.getReference());//sss

还可以使用AtomicMarkableReference判断是否修改过

        AtomicMarkableReference<String> atomicReference=new AtomicMarkableReference<>("abc",true);
        
        atomicReference.compareAndSet("abc","sss",true,false);
        System.out.println(atomicReference.getReference());//sss

原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

字段更新器

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicReferenceFieldUpdater

原子累加器

  • LongAdder

Unsafe

unsafe没法直接获取,需要通过反射的方式

public class UnsafeTest {
    //获取Unsafe实例
    static Unsafe unsafe;

    //记录偏移值
    static long stateOffset;

    private volatile long state = 0;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            //返回指定变量在所属类中的内存偏移地址
            stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));
        } catch (Exception e) {
            System.out.println(e.getLocalizedMessage());
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //创建实例
        UnsafeTest test = new UnsafeTest();
        //比较对象中偏移量为offset的值是否与expect相等
        Boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(success);
    }
}

共享模型之不可变

在多线程下使用SimpleDateFormat会报错

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("1561-05-11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }

解决方法一:加锁

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (sdf){
                    try {
                        System.out.println(sdf.parse("1561-05-11"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

解决方法二:使用JDK8提供的日期格式化方法DateTimeFormatter

        DateTimeFormatter dateFormat =DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(dateFormat.parse("1561-05-11"));
            }).start();
        }