Java并发编程

Java并发编程

并发编程的要素

原子性
原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

有序性
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

可见性
当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

线程的五大状态

创建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
在这里插入图片描述

悲观锁和乐观锁

悲观锁:每次操作都会加锁,会造成线程阻塞。

乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。

synchronized

作用

(1)原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。

(2)可见性可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。

(3)有序性有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

原理

加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令。

加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

使用

(1) 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void method() {
  //业务代码
}

(2) 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

synchronized void staic method() {
  //业务代码
}

(3) 修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

synchronized(this|object) {
  //业务代码
}

缺点

会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

Thread 方法

构造方法

方法描述
Thread()分配新的 Thread对象。
Thread(Runnable target)分配新的 Thread对象。
Thread(Runnable target, String name)分配新的 Thread对象。
Thread(String name)分配新的 Thread对象。
Thread(ThreadGroup group, Runnable target)分配新的 Thread对象。
Thread(ThreadGroup group, Runnable target, String name)分配新的 Thread对象,使其具有 target作为其运行对象,具有指定的 name作为其名称,并且属于 group引用的线程组。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)分配新的 Thread对象,使其具有 target作为其运行对象,具有指定的 name作为其名称,并且属于 group引用的线程组,并具有指定的 堆栈大小
Thread(ThreadGroup group, Runnable target, String name, long stackSize, boolean inheritThreadLocals)分配新的Thread对象,使其具有target作为其运行对象,具有指定的name作为其名称,属于group引用的线程组,具有指定的stackSize ,并且如果inheritThreadLocalstrue ,则继承inheritable thread-local变量的初始值。
Thread(ThreadGroup group, String name)分配新的 Thread对象。

常用方法

方法描述
public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public final void setName(String name)改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority)更改线程的优先级。
public final void setDaemon(boolean on)该线程标记为守护线程或用户线程。
public final void join(long millisec)等待该线程终止的时间最长为 millis 毫秒。
public void interrupt()中断线程。
public final boolean isAlive()测试线程是否处于活动状态。

上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。

静态方法

方法描述
public static void yield()暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static boolean holdsLock(Object x)当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread()返回对当前正在执行的线程对象的引用。
public static void dumpStack()将当前线程的堆栈跟踪打印至标准错误流。

线程实现的方法

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

下面的这个是我们经常使用的构造方法:

Thread(Runnable threadOb,String threadName);

实现Runnable接口

package ThreadStudy;

public class Threadtest{
    public static void main(String args[]) throws Exception {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();

        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}

class RunnableDemo implements Runnable {
    private Thread t;
    private String threadName;

    RunnableDemo(String name) {
        threadName = name;
        System.out.println("Creating " +  threadName);
    }

    public void run() {
        System.out.println("Running " +  threadName);
        try {
            for(int i = 4; i > 0; i --) {
                System.out.println("Thread: " + threadName + ", " + i);
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }

    public void start() {
        System.out.println("Starting " +  threadName);
        if(t == null) {
            t = new Thread(this, threadName);
            t.start();
        }
    }
}

继承Thread类

创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。

继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

package ThreadStudy;

public class Threadtest{
    public static void main(String args[]) throws Exception {
        ThreadDemo T1 = new ThreadDemo( "Thread-1");
        T1.start();

        ThreadDemo T2 = new ThreadDemo( "Thread-2");
        T2.start();
    }
}

class ThreadDemo extends Thread {
    private Thread t;
    private String threadName;

    ThreadDemo(String name) {
        threadName = name;
        System.out.println("Creating " +  threadName);
    }

    public void run() {
        System.out.println("Running " +  threadName);
        try {
            for(int i = 4; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", " + i);
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }

    public void start() {
        System.out.println("Starting " +  threadName);
        if (t == null) {
            t = new Thread(this, threadName);
            t.start();
        }
    }
}

通过 Callable 和 Future 创建线程

(1)创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

(2)创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

(3)使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

(4)调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

package ThreadStudy;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Threadtest{
    public static void main(String args[]) throws Exception {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        for(int i = 0; i < 100; i ++){
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
            if(i == 20) new Thread(ft,"有返回值的线程").start();
        }
        try {
            System.out.println("子线程的返回值:" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class CallableThreadTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int i;
        for(i = 0; i < 100; i ++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }
}

创建线程的三种方式的对比

(1)采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

(2)使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

线程之间的协作

wait/notify/notifyAll

这一组是 Object 类的方法

需要注意的是:这三个方法都必须在同步的范围内调用

  • wait

当前线程必须拥有该对象的监视器。线程释放此监视器的所有权并等待,直到另一个线程通过调用 notify 方法或 notifyAll 方法通知在此对象的监视器上等待的线程唤醒。然后线程等待直到它可以重新获得监视器的所有权并恢复执行。注意会释放当前对象的锁。

// wait有三种方式的调用
wait() // 等待唤醒
wait(long timeout) // 一定时间后一定会醒
wait(long timeout,long nanos) // 附加纳秒的时间

例如

synchronized(t1) {
    t1.wait();
}
  • notify

唤醒正在此对象的监视器上等待的单个线程。如果有任何线程正在等待该对象,则选择其中一个被唤醒。该选择是任意的,并由实施自行决定。

例如

synchronized (this) {
    this.notify();
}
  • notifyAll

唤醒正在此对象的监视器上等待的所有线程。 线程通过调用其中一个等待方法在对象的监视器上等待。 在当前线程放弃对该对象的锁定之前,被唤醒的线程将无法继续。 被唤醒的线程将以通常的方式与可能正在积极竞争以在该对象上同步的任何其他线程竞争; 例如,被唤醒的线程在成为下一个锁定该对象的线程时不享有可靠的特权或劣势。

例如

synchronized (this) {
    this.notifyAll();
}
举个例子

下述在主线程中,对对像t1进行加锁,然后继续执行,到wait等待t1对象调用notify让主线程恢复。而在t1线程中,run方法等待主线程wait释放掉t1的锁,然后执行notify。

package ThreadStudy;

public class Threadtest {
    public static void main(String args[]) throws Exception {
        ThreadA t1 = new ThreadA("t1");
        synchronized(t1) { // 为t1对象加锁
            try {
                // 启动“线程t1”
                System.out.println(Thread.currentThread().getName()+" start t1");
                t1.start();
                Thread.sleep(3000);
                // 主线程等待t1通过notify()唤醒。
                System.out.println(Thread.currentThread().getName()+" wait()");
                t1.wait();  //  当前线程等待,并释放t1的锁。
                System.out.println(Thread.currentThread().getName()+" continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class ThreadA extends Thread{
    public ThreadA(String name) {
        super(name);
    }
    public void run() {
        System.out.println("111");
        synchronized (this) {
            try {
                System.out.println("222");
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" call notify()");
            // 唤醒当前的主线程
            this.notify();
//            this.notifyAll();
        }
    }
}

输出结果

main start t1
111
main wait()
222
t1 call notify()
main continue

sleep/yield/join

Thread类的方法

  • sleep

让当前线程暂停指定时间,只是让出CPU的使用权,并不释放锁

  • yield

暂停当前线程的执行,也就是当前CPU的使用权,让其他线程有机会执行,不能指定时间。会让当前线程从运行状态转变为就绪状态,此方法在生产环境中很少会使用到。

只是提出申请释放CPU资源,至于能否成功释放由JVM决定。

由于这个特性,一般编程中用不到此方法,但在很多并发工具包中,yield()方法被使用,如AQSConcurrentHashMapFutureTask等。

调用了yield()方法后,线程依然处于RUNNABLE状态,线程不会进入堵塞状态。

  • join

等待调用 join 方法的线程执行结束,才执行后面的代码。
其调用一定要在 start 方法之后,底层还是用wait实现
使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法

主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

举个例子
package ThreadStudy;

public class Threadtest {
    public static void main(String args[]) throws Exception {
        ThreadA t1 = new ThreadA("t1");
        ThreadA t2 = new ThreadA("t2");
        t1.start();
        t2.start();
    }

}

class ThreadA extends Thread{
    public ThreadA(String name) {
        super(name);
    }
    public void run() {
        for(int i=1;i<=10;i++) {
            System.out.println(this.getName() + " " + i);
            if(i == 5) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
//            if (i == 5) Thread.yield();
        }
    }
}
package ThreadStudy;

public class Threadtest {
    public static void main(String args[]) throws Exception {
        ThreadA t1 = new ThreadA("t1");
        ThreadA t2 = new ThreadA("t2");
        t1.start();
        t1.join();
        t2.start();
    }

}

class ThreadA extends Thread{
    public ThreadA(String name) {
        super(name);
    }
    public void run() {
        for(int i=1;i<=10;i++) {
            System.out.println(this.getName() + " " + i);
        }
    }
}

多线程的使用

有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!

线程池

详解

Java并发编程:线程池的使用 - Matrix海子 - 博客园 (cnblogs.com)

使用

java通过Executors提供五种线程池,线程工厂ThreadFactory (Java SE 11 & JDK 11 ) (runoob.com)

ExecutorService

newCachedThreadPool,newFixedThreadPool,newSingleThreadExecutor,newWorkStealingPool创建会返回ExecutorService。

常用方法

ExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

变量和类型方法描述
booleanawaitTermination(long timeout, TimeUnit unit)阻止所有任务在关闭请求之后完成执行,或发生超时,或者当前线程被中断,以先发生者为准。
List<Future>invokeAll(Collection<? extends Callable> tasks)执行给定的任务,返回完成所有状态和结果的Futures列表。
List<Future>invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit)执行给定的任务,返回一个Futures列表,在完成或超时到期时保持其状态和结果,以先发生者为准。
TinvokeAny(Collection<? extends Callable> tasks)执行给定的任务,返回已成功完成的任务的结果(即,不抛出异常),如果有的话。
TinvokeAny(Collection<? extends Callable> tasks, long timeout, TimeUnit unit)执行给定的任务,返回已成功完成的任务的结果(即,不抛出异常),如果在给定的超时之前已经执行了任何操作。
booleanisShutdown()如果此执行程序已关闭,则返回 true
booleanisTerminated()如果关闭后所有任务都已完成,则返回 true
voidshutdown()启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。
ListshutdownNow()尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。是否能够成功关闭它是无法保证的
Future<?>submit(Runnable task)提交Runnable任务以执行并返回表示该任务的Future。
Futuresubmit(Runnable task, T result)提交Runnable任务以执行并返回表示该任务的Future。
Futuresubmit(Callable task)提交值返回任务以执行并返回表示任务的挂起结果的Future。
ScheduledExecutorService

newScheduledThreadPool 创建返回 ScheduledExecutorService。 是ExecutorService的子类

常用方法
变量和类型方法描述
ScheduledFuture<?>schedule(Runnable command, long delay, TimeUnit unit)提交在给定延迟后启用的一次性任务。
ScheduledFutureschedule(Callable callable, long delay, TimeUnit unit)提交一个返回值的一次性任务,该任务在给定的延迟后变为启用状态。
ScheduledFuture<?>scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)提交定期操作,该操作在给定的初始延迟后首先启用,随后在给定的时间段内启用; 也就是说,执行将在initialDelay之后开始,然后是initialDelay + period ,然后是initialDelay + 2 * period ,依此类推。
ScheduledFuture<?>scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)提交在给定的初始延迟之后首先启用的定期动作,并且随后在一次执行的终止和下一次执行的开始之间给定延迟。
newCachedThreadPool

创建一个根据需要创建新线程的线程池,但在它们可用时将重用以前构造的线程。 这些池通常会提高执行许多短期异步任务的程序的性能。 如果可用,调用execute将重用先前构造的线程。 如果没有可用的现有线程,则将创建一个新线程并将其添加到池中。 未使用60秒的线程将终止并从缓存中删除。 因此,长时间闲置的池不会消耗任何资源。

构造方法
变量和类型方法描述
static ExecutorServicenewCachedThreadPool()创建一个根据需要创建新线程的线程池,但在它们可用时将重用以前构造的线程。
static ExecutorServicenewCachedThreadPool(ThreadFactory threadFactory)创建一个根据需要创建新线程的线程池,但在它们可用时将重用以前构造的线程,并在需要时使用提供的ThreadFactory创建新线程。
常用方法

即为ExecutorService的方法ExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

举个例子
ExecutorService executorService = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
for(int i = 1; i <= 10; i ++) {
    if(i % 2 == 0) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        });
    }else {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
    }
}
//        executorService.shutdown();
List<Runnable> a = executorService.shutdownNow();
System.out.println(a.size());
System.out.println(executorService.isShutdown());
ExecutorService executorService = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
//        String result = executorService.invokeAny(callables);
//        System.out.println("result = " + result);
List<Future<String>> result = executorService.invokeAll(callables);
for(Future<String> i : result) {
    String res = i.get();
    System.out.println(res);
}
executorService.shutdown();
newFixedThreadPool

创建一个线程池,该池重用在共享的无界队列中运行的固定数量的线程。 在任何时候,最多nThreads线程都将处于活动状态。 如果在所有线程都处于活动状态时提交了其他任务,则它们将在LinkedBlockingQueue阻塞队列中等待,直到线程可用。 如果任何线程由于在关闭之前执行期间的故障而终止,则如果需要执行后续任务,则新线程将取代它。 池中的线程将一直存在,直到它明确为shutdown

构造函数
变量和类型方法描述
static ExecutorServicenewFixedThreadPool(int nThreads)创建一个线程池,该池重用在共享的无界队列中运行的固定数量的线程。
static ExecutorServicenewFixedThreadPool(int nThreads, ThreadFactory threadFactory)创建一个线程池,该线程池重用在共享的无界队列中运行的固定数量的线程,使用提供的ThreadFactory在需要时创建新线程。
常用方法

即为ExecutorService的方法ExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

举个例子
ExecutorService executorService = Executors.newFixedThreadPool(5,Executors.defaultThreadFactory());
for(int i = 1; i <= 10; i ++) {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    });
}
//        executorService.shutdown();
List<Runnable> a = executorService.shutdownNow();
System.out.println(a.size());
System.out.println(executorService.isShutdown());
ExecutorService executorService = Executors.newFixedThreadPool(2,Executors.defaultThreadFactory());
Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
//        String result = executorService.invokeAny(callables);
//        System.out.println("result = " + result);
List<Future<String>> result = executorService.invokeAll(callables);
for(Future<String> i : result) {
    String res = i.get();
    System.out.println(res);
}
executorService.shutdown();
newSingleThreadExecutor

创建一个Executor,它使用一个在无界队列中运行的工作线程。 (但请注意,如果此单个线程由于在关闭之前执行期间的故障而终止,则在需要执行后续任务时将使用新的线程。)保证任务顺序执行,并且不会有多个任务处于活动状态在任何给定的时间。 与其他等效的newFixedThreadPool(1)不同,保证返回的执行程序不可重新配置以使用其他线程。

构造函数
变量和类型方法描述
static ExecutorServicenewSingleThreadExecutor()创建一个Executor,它使用一个在无界队列中运行的工作线程。
static ExecutorServicenewSingleThreadExecutor(ThreadFactory threadFactory)创建一个Executor,它使用一个在无界队列中运行的工作线程,并在需要时使用提供的ThreadFactory创建一个新线程。
常用方法

即为ExecutorService的方法ExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

举个例子
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
for(int i = 1; i <= 10; i ++) {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    });
}
//        executorService.shutdown();
List<Runnable> a = executorService.shutdownNow();
System.out.println(a.size());
System.out.println(executorService.isShutdown());
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
//        String result = executorService.invokeAny(callables);
//        System.out.println("result = " + result);
List<Future<String>> result = executorService.invokeAll(callables);
for(Future<String> i : result) {
    String res = i.get();
    System.out.println(res);
}
executorService.shutdown();
newWorkStealingPool

创建一个线程池,该线程池维护足够的线程以支持给定的并行度级别,并可以使用多个队列来减少争用。 并行度级别对应于主动参与或可用于任务处理的最大线程数。 实际线程数可能会动态增长和缩小。 工作窃取池不保证提交任务的执行顺序。

构造函数
变量和类型方法描述
static ExecutorServicenewWorkStealingPool()使用 available processors的数量作为其目标并行度级别创建工作窃取线程池。
static ExecutorServicenewWorkStealingPool(int parallelism)创建一个线程池,该线程池维护足够的线程以支持给定的并行度级别,并可以使用多个队列来减少争用。
常用方法

即为ExecutorService的方法ExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

举个例子
ExecutorService executorService = Executors.newWorkStealingPool();
for(int i = 1; i <= 10; i ++) {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    });
}
//        executorService.shutdown();
List<Runnable> a = executorService.shutdownNow();
System.out.println(a.size());
System.out.println(executorService.isShutdown());
executorService.shutdown();
 ExecutorService executorService = Executors.newWorkStealingPool();
Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
//        String result = executorService.invokeAny(callables);
//        System.out.println("result = " + result);
List<Future<String>> result = executorService.invokeAll(callables);
for(Future<String> i : result) {
    String res = i.get();
    System.out.println(res);
}
executorService.shutdown();
newScheduledThreadPool

创建一个线程池,可以调度命令在给定的延迟后运行,或者定期执行。

构造函数
变量和类型方法描述
static ScheduledExecutorServicenewSingleThreadScheduledExecutor()创建一个单线程执行程序,可以调度命令在给定的延迟后运行,或定期执行。
static ScheduledExecutorServicenewSingleThreadScheduledExecutor(ThreadFactory threadFactory)创建一个单线程执行程序,可以调度命令在给定的延迟后运行,或定期执行。
常用方法

即为 ScheduledExecutorServiceScheduledExecutorService (Java SE 11 & JDK 11 ) (runoob.com)

举个例子
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5, Executors.defaultThreadFactory());
// 定时执行一次的任务,延迟1s后执行
executorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ", delay 1s");
    }
}, 1, TimeUnit.SECONDS);

// 周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
executorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ", every 3s");
    }
}, 2, 3, TimeUnit.SECONDS);

// 固定延时,初始延时1秒,然后执行完一次之后,下一次执行间隔固定延时。
executorService.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        long start = new Date().getTime();
        System.out.println("scheduleWithFixedDelay 开始执行时间:" +
                           DateFormat.getTimeInstance().format(new Date()));
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = new Date().getTime();
        System.out.println("scheduleWithFixedDelay执行花费时间=" + (end - start) / 1000 + "m");
        System.out.println("scheduleWithFixedDelay执行完成时间:"
                           + DateFormat.getTimeInstance().format(new Date()));
        System.out.println("======================================");
    }
}, 1, 2, TimeUnit.SECONDS);

方法对比

工厂方法corePoolSizemaximumPoolSizekeepAliveTimeworkQueue
newCachedThreadPool0Integer.MAX_VALUE60sSynchronousQueue
newFixedThreadPoolnThreadsnThreads0LinkedBlockingQueue
newSingleThreadExecutor110LinkedBlockingQueue
newScheduledThreadPoolcorePoolSizeInteger.MAX_VALUE0DelayedWorkQueue

五种线程池的适应场景

  1. newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
  2. newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
  3. newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。
  4. newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
  5. newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

valitate 关键字

java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。保证可见性,不保证原子性。

原理

  1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令
  2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
  3. 它会强制将对缓存的修改操作立即写入主存
  4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效

使用场景

下面列举两个使用场景

  • 状态标记量
  • 双重检查(单例模式)

举个例子

下面是单例模式中的双重锁

public class TestInstance{
	private volatile static TestInstance instance;
	
	public static TestInstance getInstance(){        //1
		if(instance == null){                        //2
			synchronized(TestInstance.class){        //3
				if(instance == null){                //4
					instance = new TestInstance();   //5
				}
			}
		}
		return instance;                             //6
	}
}

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a. memory = allocate() //分配内存
 
b. ctorInstanc(memory) //初始化对象
 
c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

CAS

AtomicBoolean,AtomicInteger,AtomicLong以及 Lock 相关类等底层就是用 CAS实现的,在一定程度上性能比 synchronized 更高。

CAS的定义

CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值 (B) 。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

CAS存在的作用

如果只是用 synchronized 来保证同步会存在以下问题
synchronized 是一种悲观锁,在使用上会造成一定的性能问题。在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。

实现原理

Java不能直接的访问操作系统底层,是通过native方法(JNI)来访问。CAS底层通过Unsafe类实现原子性操作。

可以看理解CAS算法在JAVA中的作用 - 寂静沙滩 - 博客园 (cnblogs.com)

CAS的缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

(1)ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A - 2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

(2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

(3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

举个例子

正确得到结果

package ThreadStudy;

import java.util.concurrent.atomic.AtomicInteger;

public class Threadtest{
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String args[]) throws Exception {
        for(int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //每个线程让count自增10000次
                    for (int i = 0; i < 10000; i++) {
                        count.incrementAndGet();
                    }
                }
            },"Thread " + i).start();
        }
        // 控制两个线程执行完,>2是因为当前有main线程和Monitor Ctrl-Break线程
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

不能正确得到结果

package ThreadStudy;

import java.util.concurrent.atomic.AtomicInteger;

public class Threadtest{
    private static int count = 0;
    public static void main(String args[]) throws Exception {
        for(int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //每个线程让count自增10000次
                    for (int i = 0; i < 10000; i++) {
                        count ++;
                    }
                }
            },"Thread " + i).start();
        }
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

参考

https://www.jianshu.com/p/01188fa8e511

synchronized详解 - 三分恶 - 博客园 (cnblogs.com)

Java 多线程编程 | 菜鸟教程 (runoob.com)

java.base (Java SE 11 & JDK 11 ) (runoob.com)

Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)_夏日清风-CSDN博客_java volatile

Volatile 原理和使用场景解析 - 程序员自由之路 - 博客园 (cnblogs.com)

理解CAS算法在JAVA中的作用 - 寂静沙滩 - 博客园 (cnblogs.com)

Java并发编程:线程池的使用 - Matrix海子 - 博客园 (cnblogs.com)

Java线程池:ExecutorService 的理解与使用 - 云+社区 - 腾讯云 (tencent.com)

Java 五种线程池,JDK1.8新增newWorkStealingPool_tjbsl的博客-CSDN博客_jdk1.8线程池

五种线程池的对比与使用 - 简书 (jianshu.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值