1 基础概念
1.1 CPU核心数和线程数的关系
1.1.1 CPU与线程数量
cpu个数:是指物理上,也及硬件上的核心数;
核数:是逻辑上的,简单理解为逻辑上模拟出的核心数;
逻辑核心数:线程数=1:1 ;
使用了超线程技术后---> 1:2
1.1.2 CPU和Java线程的关系
(1) 单个cpu线程在同一时刻只能执行单一Java程序,也就是一个线程
(2) 单个线程同时只能在单个cpu线程中执行
(3) 线程是操作系统最小的调度单位,进程是资源(比如:内存)分配的最小单位
(4)Java中的所有线程在JVM进程中,CPU调度的是进程中的线程
(5)Java多线程并不是由于cpu线程数为多个才称为多线程,当Java线程数大于cpu线程数,操作系统使用时间片机制,采用线程调度算法,频繁的进行线程切换。
1.2 CPU时间片轮转机制
1.2.1 时间片轮转调度
是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
1.2.2 进程切换(process switch)\上下文切换(context switch)
上下文切换也需要耗费一定的时间,当线程数多于CPU核数时,CPU就会使用上下文切换操作来保证给每个线程分配时间片段,频繁的上下文切换会导致系统性能的降低,所以线程数需要控制在合理的范围内。
结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100毫秒通常是一个比较合理的折中。
1.3 什么是进程和线程
进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:CPU调度的最小单位,必须依赖进程而存在。
1.4 澄清并行和并发
并发:并发的关键是你有处理多个任务的能力,不一定要同时。同一时刻,可以同时处理事情的能力 。
并行:并行的关键是你有同时处理多个任务的能力。 与单位时间相关,在单位时间内可以处理事情的能力。
1.5 高并发编程的意义、好处和注意事项
好处:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
问题:
线程共享资源,存在冲突;
容易导致死锁;
启用太多的线程,就有搞垮机器的可能
2 Java中线程的创建方式
2.1 实现Runnable
通过实现Runnable接口,重写run()方法。然后借助Thread的start()方法开启线程,调用run()方法是不会开启新线程的,只是一次方法调用而已。
public class CreateThreads implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
public static void main(String[] args) {
CreateThreads createThreads = new CreateThreads();
Thread thread = new Thread(createThreads);
thread.start();
}
}
2.2 继承Thread
继承Thread类,重写run()方法。
public class CreateThreads extends Thread{
@Override
public void run() {
System.out.println("继承Thread创建线程");
}
public static void main(String[] args) {
CreateThreads createThreads = new CreateThreads();
Thread thread = new Thread(createThreads);
thread.start();
}
}
注意:
①继承Thread类,重写run()方法,其本质上与实现Runnable接口的方式一致,因为Thread类本身就实现了Runnable接口
②public class Thread implements Runnable 。再加上java中多实现,单继承的特点,在选用上述两种方式创建线程时,应该首先考虑第一种(通过实现Runnable接口的方式)。
2.3 实现Callable接口
通过Runnable与Thread的方式创建的线程,是没有返回值的。然而在有些情况下,往往需要其它线程计算得到的结果供给另外线程使用( 例如:计算1+100的值,开启三个线程,一个主线程,两个计算线程,主线程需要获取两个计算线程的结算结果(一个计算线程计算1+2+...+50,另外一个线程计算51+52+..+100),进行相加,从而得到累加结果),这个时候可以采用Runnable与Thread的方式创建的线程,并通过自行编写代码实现结果返回,但是不可避免的会出现黑多错误和性能上的问题。基于此,JUC(java.util.concurrent)提供了解决方案,实现Callable的call()方法(这个类似Runnable接口),使用Future的get()方法进行获取。
下面开始使用Callable、与Future创建多线程。创建过程为:1、自定义一个类实现Callable接口,重写call()方法;2、使用JUC包下的ExecutorService,生成一个对象,主要使用submit()方法,返回得到Future对象(关于JUC包下的诸如ExecutorService解析使用,请关注博主的后序文章);3、采用Future的get()获取返回值。
/**
*
* @author dongyue
* 演示实现Callable接口,获取多线程的返回值
*/
public class CreateThreadByFutureAndCallable implements Callable{
public static void main(String[] args) throws InterruptedException, ExecutionException {
//生成具有两个线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
//调用executorService.submit()方法获取Future
Future result1 = fixedThreadPool.submit(new CreateThreadByFutureAndCallable());
Future result2 = fixedThreadPool.submit(new SubThread());
//使用Future的get()方法等待子线程计算完成返回的结果
//get方法会在运算结束后将结果返回,否则该方法就是阻塞的
int result = result1.get() + result2.get();
//关闭线程池
fixedThreadPool.shutdown();
//打印结果
System.out.println(result);
}
@Override
public Integer call() throws Exception {
int count = 0;
for(int i=0;i<50;i++) {
count += i;
}
Thread.sleep(5000);
return count;
}
}
class SubThread implements Callable{
@Override
public Integer call() throws Exception {
int count = 0;
for(int i=51;i<100;i++) {
count += i;
}
return count;
}
}
3 线程的停止方式
3.1 线程自然终止:自然执行完或抛出未处理异常
3.2 stop(),resume(),suspend()已不建议使用,stop()会导致线程不会正确释放资源,suspend()容易导致死锁。
3.3 java线程是协作式,而非抢占式
调用一个线程的interrupt() 方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身逻辑决定,开发者可以通过isInterrupted() 判定当前线程是否处于中断状态。
static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。
方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
4 线程的流程状态
线程的五大状态分别为:创建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)。
(1)新建状态:即单纯地创建一个线程,创建线程有三种方式
(2)就绪状态:在创建了线程之后,调用Thread类的start()方法来启动一个线程,即表示线程进入就绪状态!
(3)运行状态:当线程获得CPU时间,线程才从就绪状态进入到运行状态!
(4)阻塞状态:线程进入运行状态后,可能由于多种原因让线程进入阻塞状态,如:调用sleep()方法让线程睡眠,调用wait()方法让线程等待,调用join()方法、suspend()方法(它现已被弃用!)以及阻塞式IO方法。
(5)死亡状态:run()方法的正常退出就让线程进入到死亡状态,还有当一个异常未被捕获而终止了run()方法的执行也将进入到死亡状态!
注意:线程的优先级
取值为1~10,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段
5 线程的常用方法
5.1 方法
start():start方法是Thread 类的方法,在这个方法中会调用native方法(start0())来启动线程,为该线程分配资源。
sleep():使当前线程进入阻塞,可设置阻塞时间,sleep方法在进入阻塞队列时不会释放当前线程所持有的锁。
yield():该方法会让当前线程交出cpu权限,但是不能确定具体时间,和sleep方法一样不会释放当前线程所持有的锁,该方法会让线程直接进入就绪状态,很好理解。目的是为了让同等优先级的线程获得cpu执行的机会。
join():线程A,执行了线程B的join方法,线程A必须要等待B执行完成了以后,线程A才能继续自己的工作
wait() ,notifyAll(),notify():
这3个方法一般一同出现。且都是Object类的方法,用来辅助操作线程类。
这3个方法都必须在同步代码块中,不然就会报错。
wait()方法会让当前线程释放锁,并进入到条件等待队列。
notify()方法会随机唤醒条件等待队列的任意一个线程,并将其放到锁池里面。
notifyAll()方法则是唤醒条件等待队列的所有线程,并将其都放到锁池里面。
而锁池里面的线程来竞争所需要的对象锁,成功获取到锁的线程将加入到就绪队列里面。
注意:一般建议用notifyAll(),因为notify()方法会随机唤醒条件等待队列的任意一个线程,并不能保证唤醒开发者想要的线程。
interrupt(),isInterrupted(),interrupted():
这3个方法表示线程的中断,这个很有意思,分多钟情况讨论。(在java中,中断不是强制停止改线程,而是给线程一个信号,让其自行处理何时退出)
isInterrupted:就是返回对应线程的中断标志位是否为true。
interrupted:返回当前线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用
interrupted(),第一次返回的结果为true,第二次一般就是false 。
interrupt:表示中断对应的线程。
中断线程分情况讨论:
1.还没start,或者已经结束,无效果
2.运行中,中断无效,直到只能设置个中断位,直到线程走完或者进入阻塞。
3.锁池,中断无效。
4.阻塞,等待,会抛出异常,可以中断。
ps(io等待大多都是可以中断的,但是inputStream的read不会相应中断。)
中断不好使,因此最好自己在线程类里面提供关闭方法。
5.2 调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?
线程在执行yield()以后,持有的锁是不释放的
sleep()方法被调用以后,持有的锁是不释放的
调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,当wait方法返回的时候,线程会重新持有锁
调动方法之前,必须要持有锁,调用notify()方法本身不会释放锁的
6 守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
值得一提的是,守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。下面的方法就是用来设置守护线程的。
Thread daemonTread = new Thread();
// 设定 daemonThread 为 守护线程,default false(非守护线程)
daemonThread.setDaemon(true);
// 验证当前线程是否为守护线程,返回 true 则为守护线程
daemonThread.isDaemon();
这里有几点需要注意:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。
因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了。
注意:守护线程和主线程共死,finally也不能保证一定执行
7 synchronized关键字
在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:
如图,synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
8 volatile关键字,最轻量的同步机制
8.1 概述
volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。
8.2volatile特性
volatile具有可见性、有序性,不具备原子性。
注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,这一点在面试中也是非常容易问到的点。
下面来分别看下可见性、有序性、原子性:
原子性:如果你了解事务,那这个概念应该好理解。原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成那没毛病,如果只执行了一部分,那对不起,你得撤销(即事务中的回滚)已经执行的部分。
可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2...线程n能够立即读取到线程1修改后的值。
有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。(本文不对指令重排作介绍,但不代表它不重要,它是理解JAVA并发原理时非常重要的一个概念)。
8.3 volatile适用场景
适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。
适用于读多写少的场景。
可用作状态标志。
JDK中volatie应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。
8.4 volatile VS synchronized
volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.
9 ThreadLocal的使用
9.1 ThreadLocal是什么
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
void set(Object value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
9.2 Thread同步机制的比较
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程