文章目录
一、线程基础
1、基础概念
CPU核心数和线程的关系
核心数:线程数=1:1 ;使用了超线程技术后 > 1:2
CPU时间片轮转机制
又称为RR调度,会导致上下文切换
什么是进程和线程
进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:CPU调度的最小单位,必须依赖进程而存在。
并行和并发
并行:同一时刻,可以同时处理事情的能力
并发:与单位时间相关,在单位时间内可以处理事情的能力
高并发编程的意义、好处和注意事项
好处:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
问题:
线程共享资源,存在冲突;
容易导致死锁;
启用太多的线程,就有搞垮机器的可能
2、认识Java里的线程
(一)启动一个Java的main方法
public class OnlyMain {
public static void main(String[] args) {
// 虚拟机线程管理的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "]" + " " + threadInfo.getThreadName());
}
}
}
程序就会是多线程的,除了下面6种线程还可能会有其他的。
[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的
[5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
[4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程
[3] Finalizer // 调用对象的finalize(这个方法在gc启动,该对象被回收的时候被调用) 方法的线程
[2] Reference Handler //清除 Reference 的线程
[1] main //main 线程,用户程序入口
(二)线程创建的三种方式
public class NewThread {
/**
* 1、继承Thread类
*/
private static class UseThread extends Thread {
@Override
public void run() {
System.out.println("This is extends Thread");
}
}
/**
* 2、实现Runnable接口
*/
private static class UseRun implements Runnable {
@Override
public void run() {
System.out.println("This is implements Runnable");
}
}
/**
* 3、实现Callable接口,允许有返回值
*/
private static class UseCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("This is implements Callable");
return "CallResult";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
UseThread useThread = new UseThread();
useThread.start();
UseRun useRun = new UseRun();
Thread t = new Thread(useRun);
t.start();
t.interrupt();
UseCall useCall = new UseCall();
FutureTask<String> stringFutureTask = new FutureTask<>(useCall);
new Thread(stringFutureTask).start();
System.out.println(stringFutureTask.get());
}
}
(三)怎么样才能让Java里的线程安全停止工作
线程自然终止:自然是执行完成或者抛出异常
stop()、resume()、suspend()不建议使用,stop()会导致线程不会正确的释放资源,suspend()容易导致死锁。
Java线程是协作式的,而非抢占式的
调用一个线程的interrupt()方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置设为true,线程是否中断,由线程自身决定。
isInsterrupted()则是判定当前线程是否处于中断状态
static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。
示例
/**
*如何安全的中断线程
*/
public class EndThread {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (!isInterrupted()) {
System.out.println(threadName + " is run!");
}
System.out.println(threadName + "interrupt flag is " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseThread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
}
/**
*如何安全的中断线程
*/
public class EndRun {
private static class UseRun implements Runnable {
@Override
public void run() {
String name = Thread.currentThread().getName();
while (!Thread.currentThread().isInterrupted()) {
System.out.println(name + " is run!");
}
System.out.println(name + "interrupt flag is "
+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseRun useRun = new UseRun();
Thread thread = new Thread(useRun, "endThread");
thread.start();
Thread.sleep(20);
thread.interrupt();
}
}
注:抛出InterruptedException异常的时候,要注意中断标志位
public class HasInterruptException {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss_SSS");
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (!isInterrupted()) {
try {
System.out.println("UseThread:"+sdf.format(new Date()));
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println(threadName+" catch interrupt flag is "
+isInterrupted()+ " at "
+(sdf.format(new Date())));
//方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
interrupt();
e.printStackTrace();
}
System.out.println(threadName);
}
System.out.println(threadName + " interrupt flag is " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread interruptException = new UseThread("HasInterruptException");
interruptException.start();
System.out.println("Main:" + sdf.format(new Date()));
Thread.sleep(800);
System.out.println("Main begin interrupt thread: " + sdf.format(new Date()));
interruptException.interrupt();
}
}
(四)线程的生命周期
Java线程具有五中基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
整个的生命周期就是这几种状态的切换。
run()和start()的区别:run()是普通对象的普通方法,只有调用了start()后,Java才会将线程对象和操作系统中
实际的线程进行映射,再执行run()。
yield():让出CPU的执行权,将线程从运行状态转到就绪状态,但是下一个时间片段,该线程依然有可能被再次选中运行。
什么是守护线程(Daemon Thread)
守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
(4) 守护线程中finally语句块中的内容不能保证一定执行
public class DaemonThread {
private static class UseThread extends Thread {
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " I am extends Thread");
System.out.println(Thread.currentThread().getName() + " interrupt flag is " + isInterrupted());
}
}finally {
System.out.println(".......finally");
}
}
}
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
//useThread.interrupt();
}
}
二、线程间的共享
synchronized内置锁
对象锁:锁的是类的对象实例。
类锁:锁的是每个类的Class对象,每隔类的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。
对象锁的粒度要比类锁的粒度要细,引起线程竞争锁的情况比类锁要少的多,所以尽量别用类锁,锁的粒度越少越好。
/**
* 两个线程锁的是两个不同的对象,则两个线程可以同时运行,没有锁的竞争
*/
public class SynClzAndInst {
/**
* 使用对象锁的线程
*/
private static class InstanceSyn implements Runnable {
private SynClzAndInst synClzAndInst;
public InstanceSyn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance is running ... " + synClzAndInst);
synClzAndInst.instance();
}
}
/**
* 使用对象锁的线程
*/
private static class Instance2Syn implements Runnable{
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..."+synClzAndInst);
synClzAndInst.instance2();
}
}
/**
* 锁对象
*/
private synchronized void instance(){
SleepUtil.second(3);
System.out.println("synInstance is going..."+this.toString());
SleepUtil.second(3);
System.out.println("synInstance ended "+this.toString());
}
/**
* 锁对象
*/
private synchronized void instance2(){
SleepUtil.second(3);
System.out.println("synInstance2 is going..."+this.toString());
SleepUtil.second(3);
System.out.println("synInstance2 ended "+this.toString());
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
SynClzAndInst synClzAndInst2 = new SynClzAndInst();
Thread t2 = new Thread(new Instance2Syn(synClzAndInst2));
t1.start();
t2.start();
SleepUtil.second(1);
}
}
运行结果:TestInstance2 线程不用等TestInstance线程运行完,他们可以同时运行
TestInstance is running ... com.jason.SynClzAndInst@4e8a6c13
TestInstance2 is running...com.jason.SynClzAndInst@6d0ca4c7
synInstance is going...com.jason.SynClzAndInst@4e8a6c13
synInstance2 is going...com.jason.SynClzAndInst@6d0ca4c7
synInstance2 ended com.jason.SynClzAndInst@6d0ca4c7
synInstance ended com.jason.SynClzAndInst@4e8a6c13
/**
* 两个线程锁的是同一个的对象,则存在锁的竞争,就必须要等一个线程释放锁,另一个线程才能运行
*/
public class SynClzAndInst {
/**
* 使用对象锁的线程
*/
private static class InstanceSyn implements Runnable {
private SynClzAndInst synClzAndInst;
public InstanceSyn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance is running ... " + synClzAndInst);
synClzAndInst.instance();
}
}
/**
* 使用对象锁的线程
*/
private static class Instance2Syn implements Runnable{
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..."+synClzAndInst);
synClzAndInst.instance2();
}
}
/**
* 锁对象
*/
private synchronized void instance(){
SleepUtil.second(3);
System.out.println("synInstance is going..."+this.toString());
SleepUtil.second(3);
System.out.println("synInstance ended "+this.toString());
}
/**
* 锁对象
*/
private synchronized void instance2(){
SleepUtil.second(3);
System.out.println("synInstance2 is going..."+this.toString());
SleepUtil.second(3);
System.out.println("synInstance2 ended "+this.toString());
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
Thread t2 = new Thread(new Instance2Syn(synClzAndInst));
t1.start();
t2.start();
SleepUtil.second(1);
}
}
运行结果:TestInstance 线程必须等TestInstance2 线程运行完释放锁,TestInstance 线程才能运行
TestInstance2 is running...com.jason.SynClzAndInst@24824c
TestInstance is running ... com.jason.SynClzAndInst@24824c
synInstance2 is going...com.jason.SynClzAndInst@24824c
synInstance2 ended com.jason.SynClzAndInst@24824c
synInstance is going...com.jason.SynClzAndInst@24824c
synInstance ended com.jason.SynClzAndInst@24824c
/**
* 演示对象锁和类锁
*/
public class SynClzAndInst {
/**
* 使用类锁的线程
*/
private static class SynClass extends Thread {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " is running...");
synClass();
}
}
/**
* 使用对象锁的线程
*/
private static class InstanceSyn implements Runnable {
private SynClzAndInst synClzAndInst;
public InstanceSyn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance is running ... " + synClzAndInst);
synClzAndInst.instance();
}
}
/**
* 锁对象
*/
private synchronized void instance(){
SleepUtil.second(3);
System.out.println("synInstance is going..."+this.toString());
SleepUtil.second(3);
System.out.println("synInstance ended "+this.toString());
}
/**
* 类锁,实际是锁类的class对象
*/
private static synchronized void synClass() {
SleepUtil.second(1);
System.out.println("synClass going...");
SleepUtil.second(1);
System.out.println("synClass end...");
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
t1.start();
SynClass synClass = new SynClass();
synClass.start();
System.out.println("休眠开始");
SleepUtil.second(1);
System.out.println("休眠结束");
}
}
运行结果:TestInstance 线程和Thread-1也能同时的运行,没有锁的竞争
休眠开始
TestInstance is running ... com.jason.SynClzAndInst@22d07f5e
Thread-1 is running...
synClass going...
休眠结束
synClass end...
synInstance is going...com.jason.SynClzAndInst@22d07f5e
synInstance ended com.jason.SynClzAndInst@22d07f5e
volatile关键字
1、保证内存可见性
如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。
此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。
2、禁止指令重排序
指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序。比方说下面的代码:
int i = 1;
int j = 2;
上述的两条赋值语句在同一个线程之中,根据程序上的次序,“int i = 1;”的操作要先行发生于“int j = 2;”,但是“int j = 2;”的代码完全可能会被处理器先执行。JVM会保证在单线程的情况下,重排序后的执行结果会和重排序之前的结果一致。但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现,代码如下所示:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:
为instance分配内存
初始化instance
将instance变量指向分配的内存空间
由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。
2.1 内存屏障
volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:
屏障点 | 描述 |
---|---|
每个volatile写的前面插入一个store-store屏障 | 禁止上面的普通写和下面的volatile写重排序 |
每个volatile写的后面插入一个store-load屏障 | 禁止上面的volatile写与下面的volatile读/写重排序 |
每个volatile读的后面插入一个load-load屏障 | 禁止下面的普通读和上面的volatile读重排序 |
每个volatile读的后面插入一个load-store屏障 | 禁止下面的普通写和上面的volatile读重排序 |
上面的插入策略非常保守,但是它可以保证在任意处理器平台上的正确性。在实际执行时,编译器可以省略没必要的屏障点,同时在某些处理器上会做进一步的优化。
3、不保证原子性
需要重点说明的一点是,尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。请看以下的例子:
public class VolatileUnsafe {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
String threadName = Thread.currentThread().getName();
a = a + 1;
System.out.println(threadName + ":========" + a);
SleepUtil.ms(100);
a = a + 1;
System.out.println(threadName + ":========" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
执行结果如下:
Thread-2:========2
Thread-3:========3
Thread-0:========2
Thread-1:========4
Thread-3:========5
Thread-0:========7
Thread-1:========8
Thread-2:========7
所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,代码和结果如下
public class VolatileUnsafe {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public synchronized void run() {
String threadName = Thread.currentThread().getName();
a = a + 1;
System.out.println(threadName + ":========" + a);
SleepUtil.ms(100);
a = a + 1;
System.out.println(threadName + ":========" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果
Thread-2:========1
Thread-2:========2
Thread-1:========3
Thread-1:========4
Thread-0:========5
Thread-0:========6
Thread-3:========7
Thread-3:========8
ThreadLocal
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
可以把他理解成为一个map,类型是Map<Thread, Integer>
示例
public class UseThreadLocal {
/**
* 可以理解为 一个map,类型 Map<Thread,Integer>
*/
static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行三个线程
*/
public void startThreadArray() {
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new TestThread(i));
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
/**
*类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
*/
public static class TestThread implements Runnable {
int id;
public TestThread(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": start!!!");
Integer integer = threadLocal.get();
integer = integer + id;
threadLocal.set(integer);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}
}
public static void main(String[] args) {
UseThreadLocal useThreadLocal = new UseThreadLocal();
useThreadLocal.startThreadArray();
}
}
执行结果如下:
Thread-0: start!!!
Thread-2: start!!!
Thread-0: 1
Thread-1: start!!!
Thread-2: 3
Thread-1: 2