前言
在了解线程之前必须要知道线程与进程的关联与区别
进程
进程是资源(CPU、内存等)分配和调度的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。进程是表示资源分配的基本单位,又是调度运行的基本单位。
线程
线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。一个进程可以有多个线程,举一个不恰当的但是秒懂的例子,你在使用一个软件的时候,例如你在玩QQ飞车的时候同时拥有音乐的效果。
线程是一条可以执行的路径。多线程就是同时有多条执行路径在同时(并行)执行。
管程
Monitor监视器,是一种同步机制,同一时间,只有一个线程访问被保护的数据护着代码
Thread
Thread的状态
Thread属于java.lang包下的类,按照JDK的标注,其有6种状态:NEW(新建)、RUNNABLE(运行)、BLOCKED(阻塞 )、WAITING(等待)、TIMED_WAITING(有时间的等待)、TERMINATED(终止)
NEW表示尚未启动的线程的线程状态,比如刚刚new Thread()刚刚构造初始化完毕。
RUNNABLE
可运行线程的线程状态,即就绪状态处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(例如处理器)的其他资源。
BLOCKED表示线程阻塞状态,锁被其他线程占用正在等待锁释放,这个时候线程被操作系统挂起。说人话就是阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
WAITING表示无条件等待状态,由于调用以下方法之一,线程处于等待状态:
1.调用无超时参数的wait方法
2.调用无超时参数的Thread.join方法
3.调用LockSupport.part方法
上面三种方法都是不加超时时间的方法,调用三种其中之一后会处WAITING状态。如果没有被唤醒或等待的线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁。
处于等待状态的线程正在等待另一个线程执行特定操作,例如在线程对象A上调用了Object.wait()需要在线程对象B中调用a.notifyAll()或者a.notify()调用了join()方法的线程等待指定的线程终止TIMED_WAITING表示指定等待时间的线程线程状态,由于以指定的正等待时间调用以下方法之一,因此线程处于定时等待状态:
1.Thread.sleep(long)
2.Object.wait(long)方法
3.Thread.join(long)
4.LockSupport.parkNanos(long nanos)
5.LockSupport.parkUntil(long)
在指定的时间没有被唤醒或者等待线程没有结束,会被系统自动唤醒,正常退出。
TERMINATED表示线程终止状态,线程已执行完run方法,线程终止。其实这只是Java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。
JDK中的源代码
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
Thread的优先级
线程执行有优先级,优先级越高先执行机会越大(☆并不是一定优先执行☆)。优先级用int的priority参数表示。线程优先级最高为10,最低为1。默认为5。在线程构造初始化后,可以通过set方法设置具体值。Thread.MIN_PRIORITY=1;Thread.MAX_PRIORITY=10;Thread.NORM_PRIORITY=5
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "正在运行");
},"线程1");
Thread t2 = new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "正在运行");
},"线程2");
Thread t3 = new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "正在运行");
},"线程3");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
运行结果
上述结果核实了并非优先级最高就最先执行,只是优先执行的几率比较大而已。若设置的优先级不在1-10之间则会抛异常illegalArgumentException。
Thread常见的构造方法
public class Thread implements Runnable {
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
public Thread(ThreadGroup group, Runnable target) {
init(group, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {
init(group, target, name, stackSize);
}
}
守护线程与用户线程
平时我们说的线程默认是用户线程,守护线程相当于是用户线程的影子,默默为用户线程做其他的事情,例如垃圾回收GC就是一个守护线程。不设置守护线程默认都是用户线程,主要是Thread类中的setDaemon和isDaemon方法
代码如下所示
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "正在运行");
},"线程1");
t1.start();
System.out.println(t1.getName() + "是" + (t1.isDaemon() ? "守护线程" : "用户线程"));
System.out.println("主线程是" + (Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
}
}
运行结果
不设置默认都是用户线程
同时如果在start方法之后设置会抛异常,按照JDK中的规定必须在started之前
用户线程与用户线程之间是互相独立的
结果很明显,两者都是用户线程,虽然主线程结束了,但是用户线程相互独立,t1还没有结束,所以程序就还没有结束,而守护线程随着用户线程的死亡而死亡。
Thread常用的方法
sleep、yield、join、wait
先说明一下锁的概念
锁,这个概念比较抽象,拿到锁,就意味着拿到了CPU的执行权。拿3个人看电视来说,锁就好比遥控。A拿到遥控了,如果A仅仅是想休息一会儿,并不想放弃遥控的持有权(锁),那么就调用sleep(1000)方法。然而,管理员来了,对A说,你立刻、马上把遥控交给我,并且N秒内,不得再拥有遥控,此时就调用wait(10000)方法,调用wait后A会立刻丢失遥控的所有权(直到10秒后才会参与再次竞争),此时剩余的所有人立刻会按优先级,重新争取(锁)遥控的持有权。1、sleep:让出CPU调度,Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(阿里面试题 Sleep释放CPU,但不会释放锁,wait 也会释放cpu,因为cpu资源太宝贵了,只有在线程running的时候,才会获取cpu片段)
2、yield:让出CPU调度,执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳,yield不会释放锁。
3、join:让出CPU调度,join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行,会释放锁,因为join底层调用的是Object的wait方法,不过join()只会释放Thread的锁,不会释放线程对象的锁(可能会造成死锁)。
join方法中调用wait(0)让当前线程陷入无尽的等待,有wait等待就会有相应的notify或者notifyAll唤醒,join源码中,只会调用wait方法,并没有在结束时调用notify,这是因为线程在die的时候会自动调用自身的notifyAll方法,来释放所有因为该锁陷入等待的资源和锁。这个可以参考open JDK中的C++源码查看到,文件名为thread.cpp中有这么一段
4、wait:让出CPU调度,并且释放掉锁,wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
join方法
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread(), "A线程");
Thread t2 = new Thread(new MyThread(), "B线程");
t1.start();
t2.start();
//t1.join();
//---------主线程-----------//
for (int i = 1; i < 6; i++) {
System.out.println("主线程" + i);
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 1; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
运行结果
打开t1.join注释之后运行结果
注意一个线程只能start一次,多次illegalegalThreadStateException,例如只能出现一次t1.start(),若再次出现则会报异常
yield方法,释放CPU但是不释放锁,然后让线程去抢CPU,谁抢到谁就执行。
简而言之:
sleep释放CPU不释放锁
join抢占CPU释放锁(线程锁非对象锁)
yield释放CPU不释放锁
wait释放CPU释放锁
经典的窗口卖票问题
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args){
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread, "A窗口");
Thread t2 = new Thread(myThread,"B窗口");
Thread t3 = new Thread(myThread,"C窗口");
t1.start();
t2.start();
t3.start();
}
}
//同步代码块或者同步方法都可以
class MyThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
while(tickets>=1){
shopping();
}
}
private void shopping() {
synchronized (MyThread.class){
if(tickets>=1){
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
return;
}
}
}
}
运行结果
集合线程不安全
package com.ssy.demo;
import java.util.ArrayList;
import java.util.List;
public class JavaUtilConcurrent {
public static void main(String[] args){
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add("list" + Thread.currentThread().getName());
System.out.println(list);
},String.valueOf(i+1)).start();
}
}
}
运行结果
解决方案:
- Vector
- Collections.synchronizedList
- CopyOnWriteArrayList
package com.ssy.demo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
public class JavaUtilConcurrent {
public static void main(String[] args){
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
//推荐使用CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add("list" + Thread.currentThread().getName());
System.out.println(list);
},String.valueOf(i+1)).start();
}
}
}
线程辅助类
程序计数器CountDownLatch
package com.ssy.demo;
import java.util.concurrent.*;
public class JavaUtilConcurrent {
public static void main(String[] args){
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 1; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "号线程");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程辅助类之程序计数器");
}
}
运行结果
循环栅栏CyclicBarrier
package com.ssy.demo;
import java.util.concurrent.*;
public class JavaUtilConcurrent {
public static void main(String[] args){
CyclicBarrier cyclicBarrier = new CyclicBarrier(12,()->{
System.out.println("12个月过去了,一年结束");
});
for (int i = 0; i < 12; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + "月份过去了");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i+1)).start();
}
}
}
运行结果
信号灯Semaphore
package com.ssy.demo;
import java.util.Random;
import java.util.concurrent.*;
public class JavaUtilConcurrent {
public static void main(String[] args){
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "占住一个停车位");
int res = new Random().nextInt(5);
semaphore.release();
System.out.println(Thread.currentThread().getName() + "把车子开走了,这个停车位空了出来");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
运行结果
Future接口
定义了操作异步任务执行的一些方法,提供了异步并行计算的功能
* A {@code Future} represents the result of an asynchronous
* computation. Methods are provided to check if the computation is
* complete, to wait for its completion, and to retrieve the result of
* the computation. The result can only be retrieved using method
* {@code get} when the computation has completed, blocking if
* necessary until it is ready. Cancellation is performed by the
* {@code cancel} method. Additional methods are provided to
* determine if the task completed normally or was cancelled. Once a
* computation has completed, the computation cannot be cancelled.
* If you would like to use a {@code Future} for the sake
* of cancellability but not provide a usable result, you can
* declare types of the form {@code Future<?>} and
* return {@code null} as a result of the underlying task.
*
* <p>
* <b>Sample Usage</b> (Note that the following classes are all
* made-up.)
* <pre> {@code
* interface ArchiveSearcher { String search(String target); }
* class App {
* ExecutorService executor = ...
* ArchiveSearcher searcher = ...
* void showSearch(final String target)
* throws InterruptedException {
* Future<String> future
* = executor.submit(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* displayOtherThings(); // do other things while searching
* try {
* displayText(future.get()); // use future
* } catch (ExecutionException ex) { cleanup(); return; }
* }
* }}</pre>
*
* The {@link FutureTask} class is an implementation of {@code Future} that
* implements {@code Runnable}, and so may be executed by an {@code Executor}.
* For example, the above construction with {@code submit} could be replaced by:
* <pre> {@code
* FutureTask<String> future =
* new FutureTask<String>(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* executor.execute(future);}</pre>
*
* <p>Memory consistency effects: Actions taken by the asynchronous computation
* <a href="package-summary.html#MemoryVisibility"> <i>happen-before</i></a>
* actions following the corresponding {@code Future.get()} in another thread.
*
* @see FutureTask
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> The result type returned by this Future's {@code get} method
*/
public interface Future<V> {
//取消任务
boolean cancel(boolean mayInterruptIfRunning);
//判断任务是否被取消
boolean isCancelled();
/判断任务是否完成
boolean isDone();
//获取任务
V get() throws InterruptedException, ExecutionException;
//时间范围内的任务获取
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
callable:有返回值、可抛异常
runnable: 无返回值,不能抛异常
多线程、有返回、异步任务的结合之FutureTask
多线程:Runnable
有返回值:Callable
异步任务:Future
FutureTask的构造器:
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
}
由此FutureTask便可以实现多线程有返回值异步任务的结合
FutureTask结合线程池提高性能效率
package com.ssy.demo;
import java.util.concurrent.*;
public class JavaUtilConcurrent {
public static void main(String[] args){
threadPoolAsyncDeal();
commonThreadDeal();
}
private static void threadPoolAsyncDeal() {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
long startTime = System.currentTimeMillis();
FutureTask<String> task1 = new FutureTask<String>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task1 over";
});
threadPool.submit(task1);
FutureTask<String> task2 = new FutureTask<String>(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "task2 over";
});
threadPool.submit(task2);
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("线程池异步处理时间:" + (endTime-startTime));
threadPool.shutdown();
}
private static void commonThreadDeal() {
long startTime = System.currentTimeMillis();
try {
TimeUnit.MILLISECONDS.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("普通线程处理时间:" + (endTime-startTime));
}
}
运行结果明显表示性能提高了
Future的缺点:get容易造成程序的阻塞
Future的缺点:isDone容易会消耗无谓的CPU资源(若想要获取结果,一般以轮询的方式去获取,尽量不要阻塞)
综上所述Future的缺点表明对于获取结果并不太适合,最好可以有回调通知、创建异步任务、多个任务前后依赖组合处理(按照步骤处理的情况,类似于后一个需要前一个任务的值),因此JDK1.8提供了CompletableFuture接口
CompletableFuture
CompletionStage
- CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如: stage.thenApply(x -> square(x)).thenAccept(x -> system.out.print(x)).thenRun(0 -> system.out.printIn())
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。幻想做一个菜品的步骤。
CompletableFuture创建异步任务的四个核心静态方法(不推荐构造函数的方式)官方有注释说明
核心静态方法:
runAsync 无返回值public static CompletableFuture runAsync(Runnable runnable) ;
public static CompletableFuture runAsync(Runnable runnable, Executor executor);supplyAsync 有返回值
public static CompletableFuture supplyAsync(Supplier supplier);
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor);没有传入Executor的话,使用默认的ForkJoinPool.commonPool(),作为线程池执行异步代码。如果传入了的话就使用自定义的线程池进行处理。
public class JavaUtilConcurrent {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//未传入线程池的无返回值
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
System.out.println("默认线程池:" + Thread.currentThread().getName() + "无返回值");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(completableFuture1.get());
//传入线程池的无返回值
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
System.out.println("自定义线程池:" + Thread.currentThread().getName() + "无返回值");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
},threadPool);
System.out.println(completableFuture2.get());
//未传入线程池的有返回值
CompletableFuture<String> completableFuture3 = CompletableFuture.supplyAsync(() -> {
System.out.println("默认线程池:" + Thread.currentThread().getName() + "有返回值");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "supply的返回值";
});
System.out.println(completableFuture3.get());
//传入线程池的有返回值
CompletableFuture<String> completableFuture4 = CompletableFuture.supplyAsync(() -> {
System.out.println("自定义线程池:" + Thread.currentThread().getName() + "有返回值");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "supply的返回值";
},threadPool);
System.out.println(completableFuture4.get());
//关闭线程池
threadPool.shutdown();
}
运行结果
CompletableFuture模拟买书异步提高性能:查看一本书在不同平台的售卖价格
package com.ssy.demo;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class JavaUtilConcurrent {
public static List<Store> netMalls = Arrays.asList(
new Store("京东"),
new Store("拼多多"),
new Store("淘宝"),
new Store("天猫"),
new Store("淘集集")
);
public static List<String> getPrice(List<Store> netMalls, String bookName){
return netMalls.stream()
.map(netMall->{
return String.format("书名为"+bookName+"的书籍在%s上的售价为%.2f", netMall.getName(), netMall.calcPrice(bookName));
})
.collect(Collectors.toList());
}
public static List<String> getPriceByCompletableFuture(List<Store> netMalls, String bookName){
return netMalls.stream()
.map(
netMall->CompletableFuture.supplyAsync(
()->String.format("书名为"+bookName+"的书籍在%s上的售价为%.2f", netMall.getName(), netMall.calcPrice(bookName))
)
).
collect(Collectors.toList())
.stream()
.map(
completableFutureList->completableFutureList.join()
)
.collect(Collectors.toList());
}
public static void main(String[] args) {
long startTime1 = System.currentTimeMillis();
List<String> priceInfo1 = getPrice(netMalls, "MySQL");
priceInfo1.forEach(System.out::println);
long endTime1 = System.currentTimeMillis();
System.out.println("一步一步耗时"+(endTime1-startTime1));
System.out.println("----------------------------------------");
long startTime2 = System.currentTimeMillis();
List<String> priceInfo2 = getPriceByCompletableFuture(netMalls, "MySQL");
priceInfo2.forEach(System.out::println);
long endTime2 = System.currentTimeMillis();
System.out.println("提高效率后耗时"+(endTime2-startTime2));
}
}
class Store{
@Getter
private String name;
public Store(String name){
this.name = name;
}
public double calcPrice(String bookName){
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble()*2 + bookName.charAt(0);
}
}
运行结果如图所示
CompletableFuture常用方法
1.获得结果和触发计算
获取结果
public T get();
public T get(long timeout, TimeUnit unit);
public T join();
public T getNow(T valueIfAbsent);主动触发计算
public boolean comlete(T value);
package com.ssy.demo;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
CompletableFuture<String> task = CompletableFuture.supplyAsync(()->{
try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
return "5201314";
});
//类似于替代,task完成了就得到返回的"5201314",没有完成就得到"耐心等待"
System.out.println(task.getNow("请耐心等待"));
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(task.complete("打断了get或者join,不等待task") + "\t" + task.join());
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(()->{
return "52013145201314";
});
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(task1.complete("打断了get或者join,不等待task1") + "\t" + task1.join());
}
}
运行结果
源码(参考注释)
2.对计算结果进行处理
thenApply 类似于try—catch
计算结果存在依赖关系,两个线程串行化
handle 类似于try—catch—finally
计算结果存在依赖关系,两个线程串行化
具有返回结果
package com.ssy.demo;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
//自定义线程---默认的需要注意思考守护线程
ExecutorService threadPool = Executors.newFixedThreadPool(2);
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "去取钱");
return 10;
}, threadPool).thenApply(f -> {
System.out.println("输密码");
//int res = 1/0;
return f + 10;
}).thenApply(f -> {
System.out.println("得到钱");
return f + 10;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("我有钱啦");
}
}).exceptionally(e -> {
e.fillInStackTrace();
System.err.println("取钱失败");
return null;
});
System.out.println("main线程遛狗去啦(*^▽^*)");
System.out.println(task1.join());
System.out.println("-------------------------------");
CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "去取钱");
return 11;
}, threadPool).handle((f,e) -> {
System.out.println("输密码");
//int res = 1/0;
return f + 11;
}).handle((f,e) -> {
System.out.println("得到钱");
return f + 11;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("我有钱啦");
}
}).exceptionally(e -> {
e.fillInStackTrace();
System.err.println("取钱失败");
return null;
});
System.out.println("main线程打牌去啦(*^▽^*)");
System.out.println(task2.join());
threadPool.shutdown();
}
}
运行结果
打开注释后的运行如图所示
3.对计算结果进行消费
thenAccpet
接收任务的处理结果,并消费处理
无返回结果
补充:比较thenApply、thenRun、thenAccept
补充:CompletableFuture结合线程池(以thenRun、thenRunAsync为例)更多可以参考源码分析
总结
1.没有传入自定义线程池,都用默认线程池ForkJoinPool;
2.传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池3.备注: 有可能处理太快,系统优化切换原则,直接使用main线程处理, 其它如: thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理。
4.对计算速度进行选用
applyToEither
package com.ssy.demo;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
System.out.println("张三开始跑步了");
try {TimeUnit.MILLISECONDS.sleep(20L);} catch (InterruptedException e) {e.printStackTrace();}
return "张三 ";
});
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
System.out.println("李四开始跑步了");
try {TimeUnit.MILLISECONDS.sleep(10L);} catch (InterruptedException e) {e.printStackTrace();}
return "李四 ";
});
CompletableFuture<String> speed = task1.applyToEither(task2, f -> {
return f + "跑得更快";
});
System.out.println(speed.join());
}
}
运行结果如图
5.对计算结果进行合并
thencombine对计算结果进行合并
package com.ssy.demo;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
//谁先完成谁就等待
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
try {TimeUnit.MILLISECONDS.sleep(20L);} catch (InterruptedException e) {e.printStackTrace();}
return 1;
});
CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
try {TimeUnit.MILLISECONDS.sleep(10L);} catch (InterruptedException e) {e.printStackTrace();}
return 2;
});
CompletableFuture<Integer> combine = task1.thenCombine(task2, (ta1, ta2) -> {
System.out.println("开始合并");
return ta1 + ta2;
});
System.out.println(combine.join());
System.out.println("------------------------------");
CompletableFuture<Integer> thenCombine = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 30;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}), (x, y) -> {
return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 20;
}), (a, b) -> {
return a + b;
});
System.out.println(thenCombine.join());
}
}
运行结果如图所示
锁
对于Java的锁,不仅多而且繁杂,对于高并发编程而言,锁是无法规避的一个问题。Java中的锁可以解决很多问题,例如:线程是否安全、性能的优化、数据竞争等等,学习了解锁,才算对Java有了一定的只是储备。
乐观锁与悲观锁
乐观锁简单而言就是向着美好的方向思考,认为自己在使用数据的时候不会有别人(其它线程)来修改,适合读取数据多的场景下。在Java中通过无锁编程就可以实现,只是需要在更新的时候去判断之前有没有其他的线程来修改这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。在读数据的时候认为没有人会进行修改,一般情况下判断规则为:版本号机制Version、最常见的是CAS(Compare and Swap)算法(Java原子类中递增操作就是通过CAS自旋实现的)。
悲观锁简而言之就是很悲观,比如我在使用数据的时候,总想着别人(其它线程)要来修改自己的数据,所以在获取数据的时候,一不做二不休,直接加锁(保证数据的一致性和安全性),保证别人(其它线程)不会修改数据,就是那么悲观,例如synchronized、lock的实现类就是悲观锁的写照。所以悲观锁更适合写操作多的场景,先加锁保证写操作时候数据的正确性,显式的锁定之后再操作同步资源。
类锁和对象锁(实例锁)
类锁
类锁指的是一个类的静态方法上的锁(XXX.class),因为静态方法只有一份儿
public class User{
public static synchronized void say(){
System.out.println("这是类锁");
}
}
对象锁
对象锁指的是一个类的普通方法上的锁,随着对象的创建可以有多个普通的方法,如果一个类里面的很多普通方法都加锁了,那么对这个资源(this)进行访问的时候锁的是这个资源(this)的所有加了锁的方法,第一个持有锁的人没有释放,那么使用这个资源的其他人(其它线程)就不能访问到
public class User{
public synchronized void eat(){
System.out.println("这是对象锁");
}
}
注意
类锁和对象锁并不会发生竞争关系的,同步代码块锁的是synchronized(XXX)中的XXX
synchronized字节码
通过使用反编译的方式可以得到字节码javap -c XXX.class或者javap -v XXX.class
对于同步代码块,实现使用的是monitorenter和monitorexit指令,一般情况下是一个monitorenter两个monitorexit(表示一个正常退出一个异常退出),如果同步代码块中手动抛出了异常那么就会只有一个monitorenter和一个monitorexit
package com.ssy.demo;
public class JavaUtilConcurrent {
Object object = new Object();
public void a(){
synchronized (object){System.out.println("123");}
}
public static void main(String[] args) {}
}
package com.ssy.demo;
public class JavaUtilConcurrent {
Object object = new Object();
public void a(){System.out.println("123");throw new RuntimeException();}
public static void main(String[] args) {}
}
普通同步方法反编译后的flags中会有ACC_SYNCHRONIZED标识
package com.ssy.demo;
public class JavaUtilConcurrent {
Object object = new Object();
public synchronized void a(){
System.out.println("123");
}
public static void main(String[] args) {}
}
静态同步方法包含ACC_STATIC和ACC_SYNCHRONIZED
package com.ssy.demo;
public class JavaUtilConcurrent {
Object object = new Object();
public synchronized static void a(){
System.out.println("123");
}
public static void main(String[] args) {}
}
汇编语言角度
对于每一个对象都可以放在同步代码块中表示锁是因为每一个对象天生都带着一个监视器monitor,每一个被锁住的对象都会和monitor关联
公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的Lock lock = new ReentrantLock(true);//true表示公平锁,先来先得
package com.ssy.demo;
import java.util.concurrent.locks.ReentrantLock;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"A窗口").start();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"B窗口").start();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"C窗口").start();
}
}
class Ticket{
private int num = 20;
ReentrantLock lock = new ReentrantLock(true);//公平锁
public void sale(){
lock.lock();
try {
if(num>0){
System.out.println(Thread.currentThread().getName() + "卖出第" + (num--) + "张票,还剩下" + num + "张票");
}
} finally {
lock.unlock();
}
}
}
公平锁运行截图
非公平锁是指多个线程获取锁的顺序并不是按照中请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(基个线程一直得不到锁),Lock lock = new ReentrantLock(false);/lfalse表示非公平锁,后来的也可能先获得锁Lock lock = new ReentrantLock();//默认非公平锁
package com.ssy.demo;
import java.util.concurrent.locks.ReentrantLock;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"A窗口").start();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"B窗口").start();
new Thread(()->{
for (int i = 0; i < 55; i++) {
ticket.sale();
}
},"C窗口").start();
}
}
class Ticket{
private int num = 50;
ReentrantLock lock = new ReentrantLock();//非公平锁
public void sale(){
lock.lock();
try {
if(num>0){
System.out.println(Thread.currentThread().getName() + "卖出第" + (num--) + "张票,还剩下" + num + "张票");
}
} finally {
lock.unlock();
}
}
}
非公平锁运行截图
默认为非公平锁的原因:恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。因此非公平的效率是要比公平的效率高的。
那么问题来了什么时候用公平锁什么时候用非公平锁?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
可重入锁(递归锁)
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度上避免死锁。(可以再次进入同步锁),简而言之,一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。
可重入锁的种类
隐式锁和显示锁两种
隐式锁
隐式锁(即synchronized关键字使用的锁)默认是可重入锁,言而简之,在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。对于同步块和同步方法都支持。
synchronized重入锁的机制:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenteri时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
死锁及排查
死锁:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
死锁产生的原因:系统资源不足、资源分配不当、进程运行推进的顺序不对
死锁的排查:
1.纯命令 jps -l jstack 进程号
2.jconsole图形化界面
3.jvisualvm
LockSupport和线程中断
中断机制
首先,一个线程不应该由其他线租来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。所以,Thread.stop, Thread.suspend, Thread.resume都已经被废弃了。
其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制―—中断,也即中断标识协商机制。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竞该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断机制3个方法
void interrupt() 中断此线程
static boolean interrupted() 判断线程是否已被中断并清除当前中断状态 两件事情:1、返回当前线程的中断状态,测试当前线程是否已被中断;2、将当前线程的中断状态清零并重新设为false,清除线程的中断状态
boolean isInterrupted() 判断当前线程是否已被中断
那么如何停止中断运行中的线程呢
1.通过一个volatile变量实现
2.AtomicBoolean
3.Thread自带的中断API实例方法实现具体来说,当对一个线程,调用interrupt()时:①如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。②如果该线程阻塞的调用wait() , wait(long) , 或wait(1ong,int)的方法Object类,或的join(), join(long) , join(long,int) , sleep(long),sleep(long, int) ,这个类的方法,那么它的中断状态将被清除,并且将收到InterruptedException 。
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class JavaUtilConcurrent {
//1.通过一个volatile变量实现 对应methodVolatile
private static volatile boolean isStop = false;
//2.AtomicBoolean 对应methodAtomic
private static AtomicBoolean atomic = new AtomicBoolean(false);
public static void main(String[] args) {
methodVolatile();
methodAtomic();
methodJavaAPI();
}
//3.Thread自带的中断API实例方法实现 interrupt() + isInterrupted()
public static void methodJavaAPI(){
Thread t5 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + "的isInterrupted的值被改成了true\t该线程停止了");
break;
}
System.out.println(Thread.currentThread().getName() + "运行中---------isInterrupted");
}
},"t5");
t5.start();
try {TimeUnit.MILLISECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
t5.interrupt();//设置中断标识位为true
},"t6").start();
}
private static void methodAtomic() {
new Thread(()->{
while(true){
if(atomic.get()){
System.out.println(Thread.currentThread().getName() + "的atomic的值被改成了true\t该线程停止了");
break;
}
System.out.println(Thread.currentThread().getName() + "运行中---------atomic");
}
},"t3").start();
try {TimeUnit.MILLISECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
atomic.set(true);
},"t4").start();
}
private static void methodVolatile() {
new Thread(()->{
while(true){
if(isStop){
System.out.println(Thread.currentThread().getName() + "的isStop的值被改成了true\t该线程停止了");
break;
}
System.out.println(Thread.currentThread().getName() + "运行中---------isStop");
}
},"t1").start();
try {TimeUnit.MILLISECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
isStop = true;
},"t2").start();
}
}
interrupt、interrupted、isInterrupted源码分析
通过源码分析如果当前线程的中断状态标识位true,线程并不会立即停止,没有正常运行的将不影响结果
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 1; i <= 300; i++) {
System.out.println(i);
}
System.out.println(Thread.currentThread().getName() + "线程的中断状态02:" + Thread.currentThread().isInterrupted());//true
},"t1");
t1.start();
System.out.println(t1.getName() + "默认的线程的中断状态:" + t1.isInterrupted());//false
try {TimeUnit.NANOSECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
t1.interrupt();
System.out.println(t1.getName() + "线程的中断状态01:" + t1.isInterrupted()); //true
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(t1.getName() + "线程的中断状态03:" + t1.isInterrupted());//没有正常运行的将不影响结果,结合源码可知 false
}
}
如果该线程阻塞的调用wait() , wait(long) , 或wait(1ong,int)的方法Object类,或的join(), join(long) , join(long,int) , sleep(long),sleep(long, int) ,这个类的方法,那么它的中断状态将被清除,并且将收到InterruptedException 。(下面的代码很好的说明了这个问题,去掉注释可以结束,不去掉注释抛异常并进入死循环)
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("程序运行结束"); break;
}
System.out.println(Thread.currentThread().getName() + " is running~");
try {Thread.sleep(100L);} catch (InterruptedException e) {
e.printStackTrace();
//Thread.currentThread().interrupt();//集合源代码和注解分析
}
}
},"t1");
t1.start();
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
t1.interrupt();
}
}
Thread.interrupted()方法的理解:做两件事,判断线程是否被中断并清除当前中断状态。如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样。
意思是测试当前线程是否已被中断。线程的中断状态通过此方法清除。换言之,如果该方法要连续调用两次,则第二次调用将返回false(除非当前线程在第一次调用清除其中断状态之后、第二次呼叫检查之前再次中断)。由于线程在中断时不活动而忽略的线程中断将通过此方法返回false来反映。
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println("----------------------");
Thread.currentThread().interrupt();
System.out.println("----------------------");
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
}
}
运行结果
源码分析
总结:public void interrupt() , interrupt()方法是一个实例方法,它通知目标线程中断,也仅是设置目标线程的中断标志位为true。public boolean isInterrupted().,islnterrupted()方法也是一个实例方法它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志,public static boolean interrupted().Thread类的静态方法interrupted(),返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
LockSupport
用于创建锁和其它同步类的基本线程阻塞原语
park是阻塞,unpark是解除阻塞
线程等待和唤醒机制
3种等待和唤醒线程的方法
1.Object中的wait()等待,Object中的notify()唤醒
2.JUC包中的Condition的await()等待,使用signal()唤醒
3.LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object中的等待唤醒
注意:wait和notify必须出现在同步代码块或者同步方法里面,成对出现,而且需要先wait在notify才可以,否则将唤不醒。
newCondition中的等待唤醒
注意:await和signal必须出现在锁块里面,成对出现,而且需要先await在signal才可以,否则将唤不醒。
LockSupport中的park和unpark
类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),但与Semaphore 不同的是,许可的累加上限是1。
总结:LockSupport可以突破wait/notify的原有调用顺序:因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。先发放了凭证后续可以畅通无阻(类似于ETC)。之所以唤醒两次后阻塞两次,但最终结果还会阻塞线程,是因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark 效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Thread t5 = new Thread(() -> {
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName() + "进来了" + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "被唤醒了" + System.currentTimeMillis());
}, "t5");
t5.start();
//try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(() -> {
LockSupport.unpark(t5);
System.out.println(Thread.currentThread().getName() + "开始进行通知");
}, "t6").start();
}
public static void lockNewCondition(){
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "进来了");
condition.await();
System.out.println(Thread.currentThread().getName() + "被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t3").start();
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
lock.lock();
condition.signal();
System.out.println(Thread.currentThread().getName() + "开始进行通知");
lock.unlock();
},"t4").start();
}
public static void objWaitAndNotify(){
Object lock = new Object();
new Thread(()->{
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "进来了");
try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName() + "被唤醒了");
}
},"t1").start();
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
synchronized (lock){
lock.notify();
System.out.println(Thread.currentThread().getName() + "开始进行通知");
}
},"t2").start();
}
}
Java内存模型之JMM
首先看一下计算机硬件存储体系
因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。JVM规范中试图定义一种Java内存模型〈java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。JMM作用:1.通过JMM来实现线程和主内存之间的抽象关系。2.屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM规范下的三大特性
1.可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。
注意:系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存、线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
2.有序性
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
注意:指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生"脏读"),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。从源码到最终执行示例图
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
3.原子性
定义指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。例如转账,要么全部成功,要么全部失败。
Java对数据的读取过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
多线程先行发生原则之 happens-bofore
在JMM中,如果一个操作执行的结果需要对另一个操作可见性,或者代码重排序(在不影响结果的情况下才能将代码的顺序打乱执行,但是,一定要保证结果一致),那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上的先后关系。(先后关系,例如同时操作X,A线程执行完了更新到了主内存,B线程再操作X)
happens-bofore保证可见性和有序性的约束。它是判断数据是否存在竟争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
happens-bofore的总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
官方8条happens-bofore
1.次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。简而言之:前一个操作的结果可以被后续的操作获取,讲直白点就是前面一个操作把变量x赋值为1,那后面一个操作肯定能知道x已经变成了1。
2.锁定规则
一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作
3.volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
4.传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作c,则可以得出操作A先行发生于操作C(类似于先有爷爷再有爸爸最后有儿子,肯定可以得出爷先有爷爷再有孙子)
5.线程启动规则(Thread Start Rule)
Thread对象的start()方法先行发生于此线程的每一个动作,意思是即使线程已经创建了,但是必须start之后才能执行。
6.线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
可以通过Ttead.interrupted()检测到是否发生中断
也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送7.线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行
8.对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始(垃圾回收),finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作(必须先new出来了对象,才会被回收,没有垃圾如何回收呢),对象没有完成初始化之前,是不能调用finalized()方法的
JMM与volatile
volatile两大特点
被volatile修饰的变量的两大特点:可见性、有序性(禁止重排)(不支持原子性)
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
volatile内存屏障
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatle实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。简而言之,内存屏障是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作,执行一个排序的约束。也叫内存栅栏或栅栏指令
内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。简而言之写后读。因此内存屏障(Memory Barrier)分为读屏障(Load Barrier)和写屏障(Store Barrier)。
Unsfe.java源码中有涉猎到屏障。
四大屏障
为了保证有序性
1.重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要排序,听主人的。
2.对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
3.对于处理器的重排序,Java编译器在生成指令序列的适当位置﹐插入内存屏障指令,来禁止特定类型的处理器排序。
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排疗。这个操作保证了volatile读之后的操作不会被重排到volatilei读之前。
当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatle写之后。
当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
读屏障
在每个volatile读操作的后面插入LoadLoad,在每个volatile读操作的后面插入LoadStore
写屏障
在每个volatile写操作的前面插入StoreStore,在每个volatile写操作的后面插入StoreLoad
代码示例volatile
总结:不加volatile,没有可见性,程序不会结束,加了volatile,程序拥有可见性,会结束。保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见。
Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作:读取(read)、加载(load)、使用(use)、赋值(assign)、存储(store)、写入(write)、锁定(lock)、解锁(unlock)
read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
volatile使用场景
单一赋值可以,但是含复合运算赋值不可以(如i++之类的)
状态标志,判断业务是否结束
开销较低的读,写锁策略
DCL双端锁的发布
package com.ssy.demo;
public class JavaUtilConcurrent {
//防止指令重排
private static volatile JavaUtilConcurrent hungrySingleton;
private JavaUtilConcurrent(){}
//双检锁 Double-Checked-Locking
public static JavaUtilConcurrent getInstance(){
if(hungrySingleton == null){
synchronized (JavaUtilConcurrent.class){//1
if(hungrySingleton == null){//2
hungrySingleton = new JavaUtilConcurrent();//3
}
}
}
return hungrySingleton;
}
}
CAS(Compare-And-Swap)
比较并交换,是实现并发算法的时候常用到的一种技术。它包含三个操作数:内存位置、预期原值、更新值。执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值;如果不匹配,处理器不做任何操作。多个线程同时执行CAS操作只有一个会成功。
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B,当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来。当它重来重试的这种行为成为----自旋!!
硬件级别的保证
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
下面是对原子类的说明
this表示当前对象、valueoffset表示内存地址的偏移量、except表示预期原值、update表示更新值
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
原子引用AtomicReference
package com.ssy.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicReference;
public class JavaUtilConcurrent {
public static void main(String[] args) {
AtomicReference<User> reference = new AtomicReference<>();
User u1 = new User("张三", 20);
User u2 = new User("李四", 21);
reference.set(u1);
System.out.println(reference.compareAndSet(u1,u2) + " \t " + reference.get().toString());
System.out.println(reference.compareAndSet(u1,u2) + " \t " + reference.get().toString());
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class User{
private String name;
private Integer age;
}
运行结果如图
CAS与自旋
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU.
手动模仿自旋锁的demo
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class JavaUtilConcurrent {
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "正在运行中······");
while (!reference.compareAndSet(null,thread)){}
}
public void unLock(){
Thread thread = Thread.currentThread();
while (!reference.compareAndSet(thread,null)){}
System.out.println(Thread.currentThread().getName() + "运行结束了······");
}
public static void main(String[] args) {
JavaUtilConcurrent spinLock = new JavaUtilConcurrent();
new Thread(()->{
spinLock.lock();
try {TimeUnit.SECONDS.sleep(5L);} catch (InterruptedException e) {e.printStackTrace();}
spinLock.unLock();
},"张三").start();
try {TimeUnit.MILLISECONDS.sleep(500L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
spinLock.lock();
spinLock.unLock();
},"李四").start();
}
}
运行结果
CAS缺点
缺点
循环时间长会导致CPU的开销过大
某个线程一直没抢到,一直在空转,相当于死循环。
会出现ABA的问题
CAS会导致“ABA问题”,CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
戳记流水
ABA的demo1(带戳记流水的原子引用类)
package com.ssy.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicStampedReference;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Book java = new Book(1, "Java");
AtomicStampedReference<Book> reference = new AtomicStampedReference<>(java,1);
System.out.println(reference.getReference() + " \t " + reference.getStamp());
Book MySQL = new Book(2, "MySQL");
boolean flag;
flag = reference.compareAndSet(java,MySQL,reference.getStamp(),reference.getStamp()+1);
System.out.println(flag + " \t " + reference.getReference() + " \t " + reference.getStamp());
flag = reference.compareAndSet(MySQL,java,reference.getStamp(),reference.getStamp()+1);
System.out.println(flag + " \t " + reference.getReference() + " \t " + reference.getStamp());
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Book{
private Integer id;
private String name;
}
运行结果
ABA的demo2(带戳记流水的原子引用类)
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class JavaUtilConcurrent {
static AtomicReference<Integer> reference = new AtomicReference<>(100);
static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("张三",1);
public static void main(String[] args) {
abaEvent();
try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("--------------------------------------------------");
standardEvent();
}
public static void standardEvent(){
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "首次版本号为:" + stamp);
try {TimeUnit.MILLISECONDS.sleep(500L);} catch (InterruptedException e) {e.printStackTrace();}
stampedReference.compareAndSet("张三","李四",stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "--2次流水号为:" + stampedReference.getStamp());
stampedReference.compareAndSet("李四","张三",stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "--3次流水号为:" + stampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "首次版本号为:" + stamp);
//让t3先完成
try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
boolean flag = stampedReference.compareAndSet("张三", "张三丰", stamp, stamp+1);
System.out.println(flag + " \t " + stampedReference.getReference() + " \t " + stampedReference.getStamp());
},"t4").start();
}
public static void abaEvent(){
new Thread(()->{
reference.compareAndSet(100,101);
try {TimeUnit.MILLISECONDS.sleep(20L);} catch (InterruptedException e) {e.printStackTrace();}
reference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {TimeUnit.MILLISECONDS.sleep(200L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(reference.compareAndSet(100,2023) + " \t " + reference.get());
},"t2").start();
}
}
运行结果
原子类操作
基本类型原子类
public final int get();//获取当前的值
public final int getAndSet(int newValue);//获取当前值,并设置新的值
public final int getAndIncrement(); //获取当前值,并自增
public final int getAndDecrement(); //获取当前值,并自减
public final int getAndAdd(int delta); //获取当前值,并加上预期的值
public final boolean compareAndSet(int except, int update);//如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
package com.ssy.demo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class JavaUtilConcurrent {
public static final int SIZE = 50;
public static void main(String[] args) {
MyNumber number = new MyNumber();
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for (int i = 1; i <= SIZE; i++) {
new Thread(()->{
try{
for (int j = 0; j < 1000; j++) {
number.addPlus();
}
} finally {
countDownLatch.countDown();
}
},"thread" + i).start();
}
//计算完成才输出结果
//try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("最终结果:" + number.atomic.get());
}
}
class MyNumber{
AtomicInteger atomic = new AtomicInteger();
public void addPlus(){
atomic.getAndIncrement();
}
}
运行结果
数组类型原子类
package com.ssy.demo;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class JavaUtilConcurrent {
public static final AtomicIntegerArray array = new AtomicIntegerArray(new int[5]);
public static void main(String[] args) {
for (int i = 0; i < array.length(); i++) {
System.out.println(array.get(i));
}
array.getAndSet(0,2023);
System.out.println(array.get(0));
}
}
引用类型原子类
AtomicReference戳记流水是携带版本号的引用类型原子类,可以解决ABA问题, 解决修改过几次(状态戳原子引用,ABAD的demo)
AtomicMarkableReference原子更新带有标记位的引用类型对象, 解决是否修改过,它的定义就是将状态戳简化为true|false,类似于一次性物品。(状态戳(true/false)原子引用)
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class JavaUtilConcurrent {
private static AtomicMarkableReference<Integer> markReference = new AtomicMarkableReference<>(100,false);
public static void main(String[] args) {
new Thread(()->{
boolean marked = markReference.isMarked();
System.out.println(Thread.currentThread().getName() + "标志位:" + marked);
try {TimeUnit.SECONDS.sleep(1L);} catch (InterruptedException e) {e.printStackTrace();}
markReference.compareAndSet(100,1000,marked,!marked);
},"t1").start();
new Thread(()->{
boolean marked = markReference.isMarked();
System.out.println(Thread.currentThread().getName() + "标志位:" + marked);
try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
boolean flag = markReference.compareAndSet(100, 1000, marked, !marked);
System.out.println(flag);
System.out.println(markReference.getReference() + " \t " + markReference.isMarked());
},"t2").start();
}
}
运行结果
对象的属性修改原子类
AtomicIntegerFieldUpdater原子更新对象中int类型字段的值、AtomicLongFieldUpdater原子更新对象中long类型字段的值、AtomicReferenceFieldUpdater原子更新对象中引用类型字段的值
以一种线程安全的方式操作非线程安全对象内的某些字段
注意:更新的对象属性必须使用public volatile 修饰符。因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
package com.ssy.demo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class JavaUtilConcurrent {
public static final int ts = 50;
public static final int loop = 100;
public static void main(String[] args) {
commonMethod();
effectiveMethod();
}
public static void commonMethod(){
Bank bank = new Bank();
CountDownLatch countDownLatch = new CountDownLatch(ts);
for (int i = 1; i <= ts; i++) {
new Thread(()->{
try {
for (int j = 1; j <= loop; j++) {
bank.addPlus();
}
} finally {
countDownLatch.countDown();
}
},"t"+i).start();
}
try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("普通的使用synchronize的方法:" + bank.m1);
}
public static void effectiveMethod(){
Bank bank = new Bank();
CountDownLatch countDownLatch = new CountDownLatch(ts);
for (int i = 1; i <= ts; i++) {
new Thread(()->{
try {
for (int j = 1; j <= loop; j++) {
bank.transferMoney(bank);
}
} finally {
countDownLatch.countDown();
}
},"e"+i).start();
}
try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("类似于局部麻醉的高效方法AtomicIntegerFieldUpdater:" + bank.m2);
}
}
class Bank{
public String bankName = "China Constructor Bank";
int m1;
public volatile int m2;
public synchronized void addPlus(){
m1++;
}
AtomicIntegerFieldUpdater<Bank> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Bank.class,"m2");
public void transferMoney(Bank bank){
atomicIntegerFieldUpdater.getAndIncrement(bank);
}
}
运行结果
案例:来上班的人第一个开灯了后面来的人就不用开灯了
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class JavaUtilConcurrent {
public static final int ts = 5;
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 1; i <= ts; i++) {
new Thread(()->{
demo.setFlag(demo);
},"t"+i).start();
}
}
}
class Demo{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<Demo,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Demo.class,Boolean.class,"isInit");
public void setFlag(Demo demo){
if(atomicReferenceFieldUpdater.compareAndSet(demo,Boolean.FALSE,Boolean.TRUE)){
System.err.println(Thread.currentThread().getName() + "正在开灯···");
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName() + "完成开灯···");
}else{
System.out.println(Thread.currentThread().getName() + "等待别人开灯中···");
}
}
}
运行结果
原子操作增强类
Java8:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder。推荐使用LongAdder对象,比AtomicLong 性能更好(减少乐观锁的重试次数)
常用的API
LongAdder:一个或多个变量一起维持初始为零long总和。 当更新(方法add(long) )跨线程竞争时,变量集可以动态增长以减少争用。 方法sum() (或等效地, longValue() )返回保持总和的整个变量组合的当前总和。 这个类是通常优选AtomicLong当多个线程更新时使用,用于诸如收集统计信息,不用于细粒度同步控制的共同总和。 在低更新争议下,这两类具有相似的特征。 但是,在高度争议的情况下,这一类的预期吞吐量明显高于牺牲更高的空间消耗。
点赞数的案例值效率比较
package com.ssy.demo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
public class JavaUtilConcurrent {
public static final int ts = 50;
public static final int loop = 10000;
public static void main(String[] args) {
ClickTime clickTime = new ClickTime();
long startTime;
long endTime;
startTime = System.currentTimeMillis();
CountDownLatch cdl1 = new CountDownLatch(ts);
for (int i = 0; i < ts; i++) {
new Thread(()->{
try{
for (int j = 0; j < loop*100; j++) {
clickTime.clickBySynchronized();
}
}finally {
cdl1.countDown();
}
},"sync"+(i+1)).start();
}
try {cdl1.await();} catch (InterruptedException e) {e.printStackTrace();}
endTime = System.currentTimeMillis();
System.out.println("Synchronized花费:" + (endTime-startTime) + "毫秒,结果为:" + clickTime.times);
startTime = System.currentTimeMillis();
CountDownLatch cdl2 = new CountDownLatch(ts);
for (int i = 0; i < ts; i++) {
new Thread(()->{
try{
for (int j = 0; j < loop*100; j++) {
clickTime.clickByAtomicInteger();
}
}finally {
cdl2.countDown();
}
},"atoIn"+(i+1)).start();
}
try {cdl2.await();} catch (InterruptedException e) {e.printStackTrace();}
endTime = System.currentTimeMillis();
System.out.println("AtomicInteger花费:" + (endTime-startTime) + "毫秒,结果为:" + clickTime.atomicInteger.get());
startTime = System.currentTimeMillis();
CountDownLatch cdl3 = new CountDownLatch(ts);
for (int i = 0; i < ts; i++) {
new Thread(()->{
try{
for (int j = 0; j < loop*100; j++) {
clickTime.clickByLongAccumulator();
}
}finally {
cdl3.countDown();
}
},"atoIn"+(i+1)).start();
}
try {cdl3.await();} catch (InterruptedException e) {e.printStackTrace();}
endTime = System.currentTimeMillis();
System.out.println("LongAccumulator花费:" + (endTime-startTime) + "毫秒,结果为:" + clickTime.longAccumulator.get());
startTime = System.currentTimeMillis();
CountDownLatch cdl4 = new CountDownLatch(ts);
for (int i = 0; i < ts; i++) {
new Thread(()->{
try{
for (int j = 0; j < loop*100; j++) {
clickTime.clickByLongAdder();
}
}finally {
cdl4.countDown();
}
},"atoIn"+(i+1)).start();
}
try {cdl4.await();} catch (InterruptedException e) {e.printStackTrace();}
endTime = System.currentTimeMillis();
System.out.println("LongAdder花费:" + (endTime-startTime) + "毫秒,结果为:" + clickTime.longAdder.sum());
}
}
class ClickTime{
int times;
public synchronized void clickBySynchronized(){
times++;
}
AtomicInteger atomicInteger = new AtomicInteger(0);
public void clickByAtomicInteger(){
atomicInteger.getAndIncrement();
}
LongAccumulator longAccumulator = new LongAccumulator((y,x)->y+x,0);
public void clickByLongAccumulator(){
longAccumulator.accumulate(1);
}
LongAdder longAdder = new LongAdder();
public void clickByLongAdder(){
longAdder.increment();
}
}
运行结果
关于LongAdder
LongAdder是继承Striped类的,Striped类是一个很重要的类
关于Striped类
@SuppressWarnings("serial")
abstract class Striped64 extends Number {
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
}
/** Number of CPUS, to place bound on table size */
//CPU数量,即cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Table of cells. When non-null, size is a power of 2.
*/
//cells数组,为2的幂,2,4,8,16...方便后面幂运算
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
//基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新。
transient volatile long base;
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
//创建或者扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁。
transient volatile int cellsBusy;
/**
* Package-private default constructor
*/
Striped64() {
}
/**
* CASes the base field.
*/
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
/**
* CASes the cellsBusy field from 0 to 1 to acquire lock.
*/
final boolean casCellsBusy() {
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
/**
* Returns the probe value for the current thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
/**
* Pseudo-randomly advances and records the given probe value for the
* given thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
}
LongAdder的基本思路:就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
base变量:低并发,直接累加到该变量上;cell数组:高并发,累加进各个线程自己的槽Cell[i]中
LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组clls的所有值和base都加起来作为最终结果。
其实longAdder.increment主要是add(1L)、longAccumulate(x, null, uncontended)、sum()协调的
关于add(long x)
对于longAccumulate
类比新员工入职都需要一个工号
for循环剖析
3个CASE
CASE2
CASE3:多个线程尝试CAS修改失败的线程会走到这个分支
CASE1
图解
关于sum()在并发情况下值不精确:首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
sum()
阻塞队列接口BlockingQueue
队列是先进先出,栈是后进先出
放进去满了就不行,取出来空了就不行······
对于ArrayBlockingQueue,有界数组队列。
线程池
线程池(英语: thread pool) :一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。·
线程池的优势
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的框架结构
一池多线程Executors.newFixedthreadPool
一池一线程一个任务一个任务的执行Executors.newSingleThreadPool
线程池根据需求创建,可扩容,遇强则强Executors.newCachedThreadPool
拒绝策略:
- AbortPolicy(默认):满了直接抛异常
- CallerRunsPolicy:“调用者运行”一种调用机制
- DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入
- DiscardPolicy:默默丢弃无法处理的任务,不抛异常也不处理
推荐使用自定义线程池,不要使用JDK自带的,容易OOM
package com.ssy.demo;
import com.sun.xml.internal.bind.v2.runtime.output.SAXOutput;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
ThreadPoolExecutor pool = ThreadPoolUtil.createThreadPool();
try{
for (int i = 0; i < 20; i++) {
int finalI = i;
pool.execute(()->{
System.out.println(Thread.currentThread().getName() + "······" + finalI);
});
}
} finally {
pool.shutdown();
}
}
}
class ThreadPoolUtil{
public static ThreadPoolExecutor createThreadPool(){
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5,
10,
30L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
return poolExecutor;
}
}
运行结果
ThreadLocal
package com.ssy.demo;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
//ThreadLocal下的内存泄漏,解决:remove()方法
public static void main(String[] args) {
House house = new House();
for (int i=1; i <=5 ; i++) {
new Thread(()->{
Random random = new Random();
int res = random.nextInt(5)+1;
try{
//System.out.println(Thread.currentThread().getName() + "卖出房子" + res + "套");
for (int j = 0; j < res; j++) {
house.saleHouseBySynchronized();
house.saleHouseByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "通过ThreadLocal卖出房子" + house.threadLocal.get() + "套");
} finally {
house.threadLocal.remove();
}
},"线程"+i).start();
}
try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("总共卖出"+house.count);
Demo demo = new Demo();
ExecutorService exe = Executors.newFixedThreadPool(3);
try{
for (int i = 0; i < 10; i++) {
exe.submit(()->{
try {
Integer before = demo.threadLocal.get();
demo.add();
Integer after = demo.threadLocal.get();
System.out.println(Thread.currentThread().getName() + "add之前是" + before + ",之后是" + after);
} finally {
demo.threadLocal.remove();
}
});
}
} finally {
exe.shutdown();
}
}
}
class House{
int count;
public synchronized void saleHouseBySynchronized(){
++count;
}
/*
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
*/
//和上述注释掉的是一模一样的,根据个人习惯来选择
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void saleHouseByThreadLocal(){
threadLocal.set(1+threadLocal.get());
}
}
class Demo{
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void add(){
threadLocal.set(1+threadLocal.get());
}
}
运行结果
Thread、ThreadLocal、ThreadLocalMap:JVM内部维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做kev.放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
引用
强软弱虚引用
finalize的一般合同是,当Java finalize应拟机确定不再有任何方法可以被任何尚未死亡的钱程访问时,它被调用,除非是因为最终确定其他一些准备完成的对象或类所采取的行动。finalize方法可以采取任何操作,包括使该对象再次可用于其他线程;然而,finalize的通常目的是 在对象被不可撤销地丢弃之前执行清理操作。例加,表示输入输出连接的对象的finalize方法可能会执行显式I/O事务,以在永久丢弃对象之前断开连接。
强引用(默认支持模式)
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println("垃圾回收之前:" + demo);
demo = null;
System.gc();//手动开启GC,一般不用
System.out.println("垃圾回收之后:" + demo);
}
}
class Demo{
//便于理解设计的,一般我们不会使用这个方法或者重写这个方法
@Override
public void finalize(){
System.out.println("Object类的finalize执行了,它表示的是在对象被丢弃的不可撤销之前执行的清理操作");
}
}
运行结果
软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时它会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。
package com.ssy.demo;
import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
SoftReference<Demo> softReference = new SoftReference<>(new Demo());
System.out.println("GC之前是:" + softReference.get());
System.gc();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GC之后是:" + softReference.get());
}
}
class Demo{
//便于理解设计的,一般我们不会使用这个方法或者重写这个方法
@Override
public void finalize(){
System.out.println("Object类的finalize执行了,它表示的是在对象被丢弃的不可撤销之前执行的清理操作");
}
}
运行结果
添加虚拟机参数
package com.ssy.demo;
import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
SoftReference<Demo> softReference = new SoftReference<>(new Demo());
System.gc();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GC之后,内存够用:" + softReference.get());
try {
byte[] bytes = new byte[20 * 1024 * 1024];
} catch (Exception e){
e.printStackTrace();
} finally {
System.out.println("GC之后,内存不够:" + softReference.get());
}
}
}
class Demo{
//便于理解设计的,一般我们不会使用这个方法或者重写这个方法
@Override
public void finalize(){
System.out.println("Object类的finalize执行了,它表示的是在对象被丢弃的不可撤销之前执行的清理操作");
}
}
运行结果
弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
package com.ssy.demo;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
WeakReference<Demo> weakReference = new WeakReference<>(new Demo());
System.out.println("GC之前:" + weakReference.get());
System.gc();
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GC之后:" + weakReference.get());
}
}
class Demo{
//便于理解设计的,一般我们不会使用这个方法或者重写这个方法
@Override
public void finalize(){
System.out.println("Object类的finalize执行了,它表示的是在对象被丢弃的不可撤销之前执行的清理操作");
}
}
运行结果
应用场景
虚引用
1、虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
2、PhantomReference的get方法总是返回null
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
3、处理监控通知使用
换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。
也是需要设置Java虚拟机参数-Xms10m -Xmx10m
package com.ssy.demo;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Demo demo = new Demo();
ReferenceQueue<Demo> referenceQueue = new ReferenceQueue<>();
PhantomReference<Demo> phantomReference = new PhantomReference<>(demo,referenceQueue);
//System.out.println(phantomReference.get());//永远为null
List<byte[]> list = new ArrayList<>();
new Thread(()->{
while(true){
list.add(new byte[1*1024*1024]);
try {
TimeUnit.MILLISECONDS.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(phantomReference.get() + " ----add byte[] ok");
}
},"t1").start();
new Thread(()->{
while(true){
Reference<? extends Demo> poll = referenceQueue.poll();
if(poll != null){
System.out.println("----有虚对象回收,加入了引用队列");
break;
}
}
},"t2").start();
}
}
class Demo{
//便于理解设计的,一般我们不会使用这个方法或者重写这个方法
@Override
public void finalize(){
System.out.println("Object类的finalize执行了,它表示的是在对象被丢弃的不可撤销之前执行的清理操作");
}
}
运行结果
底层源码分析
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每个Thread对象维护着一个ThreadLoclMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
(1)调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
2)调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocalMap获取value。正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
那么问题来了,ThreadLocal的源码为什么使用弱引用呢?(类比人----身份证----身份证上 的信息)
当function01方法执行完毕后,栈帧销毁强引用t也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)
。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
1、当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLogaMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为nul(t=null,那么系统GC的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为nll的Entry,就没有办法访问这些key 为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref-> Thread >ThreaLocaMap >Entry >value永远无法回收,造成内存泄漏。
2、当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
3、但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得小心。
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocaIMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
源码分析
get方法
set方法
remove方法
总结
Java对象内存布局和对象头
对象内存布局
对象头包括:对象标记mark word、类元信息又叫类型指针
对象头
默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
类元信息:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(new的对象均来自于同一个模板)
实例数据:存放类的属性(Field)数据信息,包括父类的属性信息
对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
一个空类的对象new出来的大小是16字节(忽略压缩指针的影响)
首先需要添加Java Object Layout(Java对象布局的依赖包,属于openjdk的)
<!--分析对象再JVM的大小和布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class JavaUtilConcurrent {
public static void main(String[] args) {
//Thread.currentThread();
//VM详细信息
System.out.println(VM.current().details());
System.out.println("----------------------");
//所有的对象分配的字节都是8的整数倍。
System.out.println("对齐填充:" + VM.current().objectAlignment());
System.out.println("----------------------");
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.out.println("----------------------");
Demo1 demo1 = new Demo1();
System.out.println(ClassLayout.parseInstance(demo1).toPrintable());
System.out.println("----------------------");
Demo2 demo2 = new Demo2();
System.out.println(ClassLayout.parseInstance(demo2).toPrintable());
}
}
class Demo1{
}
class Demo2{
int age;
boolean flag;
}
运行结果
GC的分代年龄:Gc年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15。
设置虚拟机参数,将Gc年龄设置为16:-XX:MaxTenuringThreshold=16,结果程序报错
压缩指针
终端输入java -XX:+PrintCommandLineFlags -version
可以看到默认配置是启动了压缩指针的 -XX:+UseCompressedClassPointers
,所以上面的类元信息只有四字节,出现了loss due to the next object alignment
的字样。
添加虚拟机参数-XX:-UseCompressedClassPointers
然后运行如下代码可以发现就没有loss due to the next object alignment的字样了。
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
Synchronized和锁升级
用锁能够实现数据的安全性,但是会带来性能下降。无锁能够基于线程并行提升程序性能,但是会带来安全性下降。如何求取平衡呢?无锁–偏向锁–轻量锁–重量锁
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。在Java早期版本中,synchronized属 于重量级锁,效率低下,因为监视器锁(monitor) 是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒-一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因,Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
Monitor可以理解为一种同步工具,也可理解为一种同步机制, 常常被描述为一个Java对 象。Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质, 因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
Monitor是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。
如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor的起始地址
Monitor的Owner子段会存放拥有相关联对象锁的线程id
Mutex Lock的切换需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间
synchronized用的锁是存在Java对象头里的Mark Word中锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位。
无锁
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Object o = new Object();
System.out.println("10进制:" + o.hashCode());
System.out.println("2进制:" + Integer.toBinaryString(o.hashCode()));
System.out.println("16进制:" + Integer.toHexString(o.hashCode()));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
// 1001011011001111100111101001101
// 1001011011001111100111101001101
}
}
偏向锁
偏向锁:单线程竞争
,当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁.
(类比一个常客吃饭习惯不变,老规矩)
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能。
在实际应用运行过程中发现,“ 锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID。
竞争成功,表示之前的线程不存在了,MarkWord 里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁。(类似于A用完了就走了,B刚好进来可以用)
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。(类似于A一直在里头,B一直CAS)
注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现:一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了。
总结:JVM不会和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。
使用Linux命令输入java -XX:+PrintFlagsInitial | grep BiasedLock*
(也可以用Git的CLI进行操作)
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
public class JavaUtilConcurrent {
public static void main(String[] args) {
Object o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
运行结果
解决方法一:睡眠时间大于4秒
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args){
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
结果为偏向锁
解决方法二:设置延时为0-XX:BiasedLockingStartupDelay=0
关闭偏向锁的延时
再次运行结果就是偏向锁
偏向锁的线程ID
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args){
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("无锁状态\n" + ClassLayout.parseInstance(o).toPrintable());
System.out.println("============有锁状态==============");
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
}
}
运行结果
可以看到,锁状态为101可偏向锁状态了,只是由于o对象未用synchronized加锁,所以线程id是空的。其余数据跟上述无锁状态一样。偏向锁带线程id情况,第一行中后面不再是0了,有了线程id的值。
偏向锁的撤销:当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁,竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行。①第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。②第一个线程执行完成synchronized方法(退出同步块), 则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
小编听说官方发布在Java15以后偏向锁逐渐废弃了,有兴趣的话可以研究研究其他JDK版本的一些变更
轻量级锁
轻量级锁: 多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寨。 本质就是CAS自旋锁。
轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程被释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位。
如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
自适应自旋锁的大致原理:线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。自适应意味着自旋的次数不是固定不变的,而是根据同一个锁上一次自旋的时间、拥有锁线程的状态来决定的。
轻量级锁的加锁
JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋CAS:不断尝试去获取锁,能不升级就不往上捅,尽量不要阻塞
轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程
偏向锁和轻量级锁区别
争夺轻量级锁失败时,自旋尝试抢占锁,轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。
重量级锁
重量级锁,指向互斥量(重量级锁)的指针
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
public class JavaUtilConcurrent {
public static void main(String[] args){
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t2").start();
}
}
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码,GC年龄了,那么问题来了,这些信息被移动到哪里去了呢?
其实,在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
对于偏向锁,在线程获取偏向锁时,会用Thread lD和epoch值(可以简单理解为时间戳)覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程ID给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word。锁释放后也会将信息写回到对象头。
当一个对象已经计算过identity hashcode,它就无法进入偏向锁状态,跳过偏向锁,直接升级轻量级锁。
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args){
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("本来是偏向锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
synchronized (o){
System.out.println("当一个对象已经计算过identity hashcode,它就无法进入偏向锁状态,跳过偏向锁,直接升级轻量级锁。");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
偏向锁过程中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁。
package com.ssy.demo;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class JavaUtilConcurrent {
public static void main(String[] args){
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("本来是偏向锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
o.hashCode();
System.out.println("偏向锁过程中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁。");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
各种锁的优缺点比较
JIT编译器对锁的优化
JIT
Just In Time Complier(一般翻译为即时编译器)
锁消除
package com.ssy.demo;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
for (int i = 0; i < 10; i++) {
new Thread(()->{
demo.m1();
},String.valueOf(i)).start();
}
}
}
//这个锁对象并没有被共用扩散到其它线程使用
//极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
class Demo{
static Object obj = new Object();
public void m1(){
//锁消除问题,JIT编译器会无视它, synchronized (o),每次new出来,不存在了,非正常的。
Object o = new Object();
synchronized (o){
System.out.println("o的哈希值为:" + o.hashCode() + "\t" + "obj的哈希值为:" + obj.hashCode());
}
}
}
锁粗化
package com.ssy.demo;
//假如方法中首尾相接,前后相邻的都是同一个锁对象
//那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
public class JavaUtilConcurrent {
public static void main(String[] args){
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println("111111");
}
synchronized (o){
System.out.println("222222");
}
synchronized (o){
System.out.println("333333");
}
synchronized (o){
System.out.println("111111");
System.out.println("222222");
System.out.println("333333");
}
}).start();
}
}
AbstractQueuedSynchronizer(AQS)
字面意思:抽象队列同步器
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
有阻塞就需要排队,实现排队必然需要队列,实质就是CLH双端队列+state变量。ReentrantLock、CountDownLatch、SemaPhore、ReentrantReadWriteLock
关于ReentrantLock.java
关于AQS的acquire方法
上述是关于lock方法,接下来看unlock方法
release、tryRelease、unparkSuccessor
对于异常情况,例如等不耐烦了,不想等了
总结
ReentrantLock、ReenReadWriteLock、StampedeLock
读写锁的定义:一个资源可以被多个读线程访问或者一个写线程访问,但是不能同时存在读写线程。读写互斥、读读共享。
Lock接口源码
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReadWriteLock源码
package java.util.concurrent.locks;
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReaWriteLock:实现了读读共享,多线程可以并发访问,大面积可以允许多个线程来读取。适合读多写少的情况,缺点:写饥饿问题、锁降级
它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁(切菜还是拍蒜选一个)。也即一个资源可以被多个读操作访问―或一个写操作访问,但两者不能同时进行。只有在读多写少情景之下,读写锁才具有较高的性能体现。
普通锁
package com.ssy.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.write(finalI +"", finalI +"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.read(finalI +"");
},String.valueOf(i)).start();
}
}
}
class Demo{
Map<String, String> map = new HashMap<>();
Lock lock = new ReentrantLock();
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read(String key){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完毕" + "\t" + map.get(key));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void write(String key, String value){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key,value);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果
读写锁
package com.ssy.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.write(finalI +"", finalI +"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.read(finalI +"");
},String.valueOf(i)).start();
}
}
}
class Demo{
Map<String, String> map = new HashMap<>();
Lock lock = new ReentrantLock();
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read(String key){
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完毕" + "\t" + map.get(key));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
public void write(String key, String value){
rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key,value);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
}
运行结果
读写互斥,读没有完成前,写不能执行
package com.ssy.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.write(finalI +"", finalI +"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()->{
demo.read(finalI +"");
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i <= 3; i++) {
int finalI = i;
new Thread(()->{
demo.write(finalI +"", finalI +"");
},"新写锁"+finalI).start();
}
}
}
class Demo{
Map<String, String> map = new HashMap<>();
Lock lock = new ReentrantLock();
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read(String key){
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
//TimeUnit.MILLISECONDS.sleep(200);
//读没有完成之前不能写
TimeUnit.MILLISECONDS.sleep(2000);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完毕" + "\t" + map.get(key));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
public void write(String key, String value){
rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key,value);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
}
运行结果
ReentrantReadWriteLock锁降级:将写入锁降级为读锁
写锁的降级,降级成为了读锁
1.如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
2.规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
3.如果释放了写锁,那么就完全转换为读锁。
package com.ssy.demo;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
//好比A线程写
writeLock.lock();
System.out.println("写入~");
writeLock.unlock();
//B线程读
readLock.lock();
System.out.println("读取~");
readLock.unlock();
}
}
运行结果
锁降级
只能降级,不能升级,只能从写锁降为读锁,不能从读锁升为写锁
无锁—独占锁—读写锁—邮戳锁
锁饥饿问题:ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了,因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。
解决:使用公平锁的策略一定程度上可以解决,new ReentrantLock(true);但是却是牺牲系统的吞吐量为代价的。另外就是以StampLock乐观读锁的方式。
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。读的过程中也允许写锁j进入。
比读写锁更快的锁:StampedeLock(邮戳锁、版本锁、票据锁)
stamp(戳记,long类型):代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
StampedLock是不可重入的
StampedLock的三种访问模式:
- Reading读模式悲观,功能和ReentrantLock的读锁类似
- Writing写模式,功能和ReentrantLock的写锁类似
- Optimistic Reading乐观读模式,无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观的认为读取时没人修改,假如被修改再升级为悲观锁模式
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
new Thread(()->{
demo.read();
},"读线程").start();
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t" + "进来了");
demo.write();
},"写线程").start();
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\tnumber = " + Demo.number);
}
}
class Demo{
static int number = 100;
static StampedLock stampedLock = new StampedLock();
//悲观读,读没有完成时写无法获取锁
public void read(){
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "\t" + "读模式准备中");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中······");
}
try{
int res = number;
System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量number的值:" + res);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
} finally {
stampedLock.unlockRead(stamp);
}
}
public void write(){
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
try{
number = number + 420;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改结束");
}
}
运行结果
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
new Thread(()->{
demo.readOptimistic();
},"读线程").start();
//暂停2s,写线程介入
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
//暂停6s,读线程完毕,写线程开始
//try {TimeUnit.SECONDS.sleep(6);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t" + "进来了");
demo.write();
},"写线程").start();
}
}
class Demo{
static int number = 100;
static StampedLock stampedLock = new StampedLock();
//悲观读,读没有完成时写无法获取锁
public void read(){
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "\t" + "读模式准备中");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中······");
}
try{
int res = number;
System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量number的值:" + res);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
} finally {
stampedLock.unlockRead(stamp);
}
}
//乐观读
public void readOptimistic(){
long stamp = stampedLock.tryOptimisticRead();
int res = Demo.number;
System.out.println("4秒前stampLock.validate(false表示修改过,true表示没有修改过)" + "\t" + stampedLock.validate(stamp));
for (int i = 1; i < 5; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中······" + i + "秒后,validate的值为:" + stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)){
System.out.println("写操作----------有人修改过");
stamp = stampedLock.readLock();
try{
System.out.println("从乐观读升级为悲观读");
res = number;
System.out.println("重新悲观读后,最后的结果为:" + res);
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + "\t number = " + res);
}
public void write(){
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
try{
number = number + 420;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改结束");
}
}
运行结果
package com.ssy.demo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
public class JavaUtilConcurrent {
public static void main(String[] args){
Demo demo = new Demo();
new Thread(()->{
demo.readOptimistic();
},"读线程").start();
//暂停2s,写线程介入
//try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
//暂停6s,读线程完毕,写线程开始
try {TimeUnit.SECONDS.sleep(6);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t" + "进来了");
demo.write();
},"写线程").start();
}
}
class Demo{
static int number = 100;
static StampedLock stampedLock = new StampedLock();
//悲观读,读没有完成时写无法获取锁
public void read(){
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "\t" + "读模式准备中");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中······");
}
try{
int res = number;
System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量number的值:" + res);
System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
} finally {
stampedLock.unlockRead(stamp);
}
}
//乐观读
public void readOptimistic(){
long stamp = stampedLock.tryOptimisticRead();
int res = Demo.number;
System.out.println("4秒前stampLock.validate(false表示修改过,true表示没有修改过)" + "\t" + stampedLock.validate(stamp));
for (int i = 1; i < 5; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中······" + i + "秒后,validate的值为:" + stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)){
System.out.println("写操作----------有人修改过");
stamp = stampedLock.readLock();
try{
System.out.println("从乐观读升级为悲观读");
res = number;
System.out.println("重新悲观读后,最后的结果为:" + res);
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + "\t number = " + res);
}
public void write(){
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
try{
number = number + 420;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改结束");
}
}
运行结果
StampedLock的缺点:不可重入、StampedLock的悲观读锁和写锁都不支持条件变量(Condition)、使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法。
分支合并框架
简单而言就是ForkJoin框架,举个例子:1+2+3+4+…+100,每次分开计算,类似于二分查找的样子,两数相差不超过10那么直接加,超过10继续拆分,最后合起来。相当于大事化小,先分后合。
一个简单的测试Demo
package com.ssy.demo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class JavaUtilConcurrent {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Demo demo = new Demo(1, 100);
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> res = pool.submit(demo);
Integer result = res.get();
System.out.println(result);
pool.shutdown();
}
}
class Demo extends RecursiveTask<Integer> {
private static final Integer VALUE = 10;
private int start;
private int end;
private int res;
public Demo(int start, int end){
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if(start-end <= VALUE){
for (int i = start; i <= end; i++) {
res = res + i;
}
} else {
int middle = (start + end) / 2;
//左边
Demo t1 = new Demo(start, middle);
//右边
Demo t2 = new Demo(middle + 1, end);
//拆分
t1.fork();
t2.fork();
//合并
res = t1.join() + t2.join();
}
return res;
}
}
运行结果