java线程安全的方法-:线程线程的概念
出品 | CSDN博客
线程
线程的概念,百度是这样解释的:
线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(Lightweight Processes),但轻量进程更多指内核线程(Kernel Thread),而把用户线程(User Thread)称为线程。
1.1 线程与进程的区别
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
也就是,进程可以包含多个线程,而线程是程序执行的最小单位。
1.2 线程的状态
1.3 Notify和Wait :
Notify和Wait 的作用
首先看源码给出的解释,这里翻译了一下:
Notify:唤醒一个正在等待这个对象的线程监控。如果有任何线程正在等待这个对象,那么它们中的一个被选择被唤醒。选择是任意的,发生在执行的酌情权。一个线程等待一个对象通过调用一个{@code wait}方法进行监视。
Notify()需要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁
Wait:导致当前线程等待,直到另一个线程调用{@link java.lang.Object#notify()}方法或{@link java.lang.Object#notifyAll()}方法。
换句话说,这个方法的行为就像它简单一样执行调用{@code wait(0)}。当前线程必须拥有该对象的监视器。
线程释放此监视器的所有权,并等待另一个线程通知等待该对象的监视器的线程,唤醒通过调用{@code notify}方法或{@code notifyAll}方法。然后线程等待,直到它可以重新取得监视器的所有权,然后继续执行。
Wait()的作用是使当前执行代码的线程进行等待,它是Object类的方法,该方法用来将当前线程置入预执行队列中,并且在Wait所在的代码行处停止执行,直到接到通知或被中断为止。
在调用Wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用Wait方法。
Wait和Sleep的区别:
1.4 Thread.sleep() 和Thread.yield()的异同
1.5 补充:死锁的概念
死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的四个必要条件(缺一不可):
我们可以根据死锁的四个必要条件破坏死锁的形成。
1.6 补充:并发和并行的区别
并发:是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状。
并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,CPU同时处理这些线程请求的能力。
区别就在于CPU是否能同时处理所有任务,并发不能,并行能。
1.7 补充:线程安全三要素
1.8 补充:如何实现线程安全
1.9 补充:保证线程安全的机制:
随着CPU核心的增多以及互联网迅速发展,单线程的程序处理速度越来越跟不上发展速度和大数据量的增长速度,多线程应运而生,充分利用CPU资源的同时,极大提高了程序处理速度。
创建线程的方法
继承Thread类:
public class ThreadCreateTest {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}
实现Runable接口:
public class RunableCreateTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
}
}
通过Callable和Future创建线程:
public class CallableCreateTest {
public static void main(String[] args) throws Exception {
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTaskfutureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法会阻塞调用的线程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
return sum;
}
}
线程池方式创建:
实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承,但可以多实现啊),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
实际开发中,阿里巴巴开发插件一直提倡使用线程池创建线程,原因在下方会解释,所以上面的代码我就只简写了一些Demo。
2.1 线程池创建线程
线程池,顾名思义,线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要由以下几个特点:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(主要)。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。
Java提供四种线程池创建方式:
通过源码我们得知ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService。
public class ThreadPoolExecutor extends AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService
2.2 ThreadPoolExecutor介绍
实际项目中,用的最多的就是ThreadPoolExecutor这个类,而《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过New ThreadPoolExecutor实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
我们从ThreadPoolExecutor入手多线程创建方式,先看一下线程池创建的最全参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueueworkQueue, ) {
ThreadFactory threadFactory,
RejectedExecutionHandler handler
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明如下:
2.2.1BlockingQueue
对于BlockingQueue个人感觉还需要单独拿出来说一下。
BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:ArrayBlockingQueue和LinkedBlockingQueue
队列的数据结构大致如图:
队列一端进入,一端输出。而当队列满时,阻塞。BlockingQueue核心方法:1. 放入数据put2. 获取数据take。常见的两种Queue:
2.2.2 ArrayBlockingQueue
基于数组实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
一段代码来验证一下:
package map;
import java.util.concurrent.*;
public class MyTestMap {
// 定义阻塞队列大小
private static final int maxSize = 5;
public static void main(String[] args){
ArrayBlockingQueuequeue = new ArrayBlockingQueue (maxSize);
new Thread(new Productor(queue)).start();
new Thread(new Customer(queue)).start();
}
}
class Customer implements Runnable {
private BlockingQueuequeue;
Customer(BlockingQueuequeue) {
this.queue = queue;
}
@Override
public void run() {
this.cusume();
}
private void cusume() {
while (true) {
try {
int count = (int) queue.take();
System.out.println("customer正在消费第" + count + "个商品===");
// 只是为了方便观察输出结果
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor implements Runnable {
private BlockingQueuequeue;
private int count = 1;
Productor(BlockingQueuequeue) {
this.queue = queue;
}
@Override
public void run() {
this.product();
}
private void product() {
while (true) {
try {
queue.put(count);
System.out.println("生产者正在生产第" + count + "个商品");
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//输出如下
/**
生产者正在生产第1个商品
生产者正在生产第2个商品
生产者正在生产第3个商品
生产者正在生产第4个商品
生产者正在生产第5个商品
customer正在消费第1个商品===
*/
2.2.3 LinkedBlockingQueue
基于链表的阻塞队列,内部也维护了一个数据缓冲队列。需要我们注意的是如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小。
LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
2.2.4 LinkedBlockingQueue和ArrayBlockingQueue的主要区别
2.2.5handler拒绝策略
Java提供了4种丢弃处理的方法,当然你也可以自己实现,主要是要实现接口:RejectedExecutionHandler中的方法。
2.2.6线程池五种状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务java线程安全的方法java线程安全的方法,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。
STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态。
TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。
TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态。
回到线程池创建ThreadPoolExecutor,我们了解了这些参数,再来看看ThreadPoolExecutor的内部工作原理:
2.3深入理解ThreadPoolExecutor
进入Execute方法可以看到:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果不小于corePoolSize,则将任务添加到workQueue队列。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。
else if (!addWorker(command, false))
reject(command);
}
AddWorker方法:
2.3.1线程池中CTL属性的作用是什么?
看源码第一反应就是这个CTL到底是个什么东东?有啥用?一番研究得出如下结论:
CTL属性包含两个概念:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int ctlOf(int rs, int wc) { return rs | wc; }
我们点击workerCount即工作状态记录值,以RUNNING为例,RUNNING = -1