java 多线程详解(持续更新)

学前小故事

一切要从CPU说起

从CPU到操作系统

从单核到多核,如何充分利用多核

从进程到线程

线程与内存

线程任务类型

从多线程到线程池

线程

实现线程

线程的生命周期

线程的执行顺序

线程的方法

线程的安全

线程的停止

线程相关[类&接口]

Callable接口

Future接口

FutureTask类

ThreadLocal类

并发集合

AtomicInteger

CopyOnWriteArrayList

ConcurrentHashMap

锁机制

互斥锁、自旋锁

乐观锁、悲观锁

公平锁、非公平锁

读锁、写锁

线程池

创建线程池

工作队列(workQueue)

线程工厂(threadFactory)

拒绝策略(handler)

线程数量设置

数据传输(IO)

同步阻塞(BIO)

同步非阻塞(NIO)

异步非阻塞(AIO)

系统

计算并发量

高并发解决方案

示例导航

线程方法相关

wait、notifyAll、sleep相关

线程安全相关

拓展

cpu与核心

用户态与内核态

Java线程池

一切要从CPU说起

你可能会有疑问,讲多线程为什么要从CPU说起呢?原因很简单,在这里没有那些时髦的概念,你可以更加清晰的看清问题的本质。

CPU并不知道线程、进程之类的概念。它只知道两件事:

• 从内存中取出指令
• 执行指令,然后回到1

在这里插入图片描述

你看,在这里CPU确实是不知道什么进程、线程之类的概念。

接下来的问题就是CPU从哪里取出指令呢?答案是来自一个被称为Program Counter(简称PC)的寄存器,也就是我们熟知的程序计数器,在这里大家不要把寄存器想的太神秘,你可以简单的把寄存器理解为内存,只不过存取速度更快而已。

PC寄存器中存放的是什么呢?这里存放的是指令在内存中的地址,什么指令呢?是CPU将要执行的下一条指令。
在这里插入图片描述
那么是谁来设置PC寄存器中的指令地址呢?

原来PC寄存器中的地址默认是自动加1的,这当然是有道理的,因为大部分情况下CPU都是一条接一条顺序执行,当遇到if、else时,这种顺序执行就被打破了,CPU在执行这类指令时会根据计算结果来动态改变PC寄存器中的值,这样CPU就可以正确的跳转到需要执行的指令了。

聪明的你一定会问,那么PC中的初始值是怎么被设置的呢?

在回答这个问题之前我们需要知道CPU执行的指令来自哪里?是来自内存,废话,内存中的指令是从磁盘中保存的可执行程序加载过来的,磁盘中可执行程序是编译器生成的,编译器又是从哪里生成的机器指令呢?答案就是我们定义的函数。

在这里插入图片描述

注意是函数,函数被编译后才会形成CPU执行的指令,那么很自然的,我们该如何让CPU执行一个函数呢?显然我们只需要找到函数被编译后形成的第一条指令就可以了,第一条指令就是函数入口。

现在你应该知道了吧,我们想要CPU执行一个函数,那么只需要把该函数对应的第一条机器指令的地址写入PC寄存器就可以了,这样我们写的函数就开始被CPU执行起来啦。

你可能会有疑问,这和线程有什么关系呢?

从CPU到操作系统

上节中我们明白了CPU的工作原理,我们想让CPU执行某个函数,那么只需要把函数对应的第一条机器执行装入PC寄存器就可以了,这样即使没有操作系统我们也可以让CPU执行程序,虽然可行但这是一个非常繁琐的过程,我们需要:

• 在内存中找到一块大小合适的区域装入程序
• 找到函数入口,设置好PC寄存器让CPU开始执行程序

这两个步骤绝不是那么容易的事情,如果每次在执行程序时程序员自己手动实现上述两个过程会疯掉的,因此聪明的程序员就会想干脆直接写个程序来自动完成上面两个步骤吧。
在这里插入图片描述

机器指令需要加载到内存中执行,因此需要记录下内存的起始地址和长度;同时要找到函数的入口地址并写到PC寄存器中,想一想这是不是需要一个数据结构来记录下这些信息:

struct *** {
   
   void* start_addr;
   int len;
   
   void* start_point;
   ...
};

接下来就是起名字时刻。

这个数据结构总要有个名字吧,这个结构体用来记录什么信息呢?记录的是程序在被加载到内存中的运行状态,程序从磁盘加载到内存跑起来叫什么好呢?干脆就叫进程(Process)好了,我们的指导原则就是一定要听上去比较神秘,总之大家都不容易弄懂就对了,我将其称为“弄不懂原则”。

就这样进程诞生了。

CPU执行的第一个函数也起个名字,第一个要被执行的函数听起来比较重要,干脆就叫main函数吧。

完成上述两个步骤的程序也要起个名字,根据“弄不懂原则”这个“简单”的程序就叫操作系统(Operating System)好啦。

就这样操作系统诞生了,程序员要想运行程序再也不用自己手动加载一遍了。
现在进程和操作系统都有了,一切看上去都很完美。

从单核到多核,如何充分利用多核

人类的一大特点就是生命不息折腾不止,从单核折腾到了多核。
在这里插入图片描述
这时,假设我们想写一个程序并且要分利用多核该怎么办呢?

有的同学可能会说不是有进程吗,多开几个进程不就可以了?听上去似乎很有道理,但是主要存在这样几个问题:

* 进程是需要占用内存空间的(从上一节能看到这一点),如果多个进程基于同一个可执行程序,那么这些进程其内存区域中的内容几乎完全相同,这显然会造成内存的浪费

计算机处理的任务可能是比较复杂的,这就涉及到了进程间通信,由于各个进程处于不同的内存地址空间,进程间通信天然需要借助操作系统,这就在增大编程难度的同时也增加了系统开销

该怎么办呢?

从进程到线程

让我再来仔细的想一想这个问题,所谓进程无非就是内存中的一段区域,这段区域中保存了CPU执行的机器指令以及函数运行时的堆栈信息,要想让进程运行,就把main函数的第一条机器指令地址写入PC寄存器,这样进程就运行起来了。
在这里插入图片描述
进程的缺点在于只有一个入口函数,也就是main函数,因此进程中的机器指令只能被一个CPU执行,那么有没有办法让多个CPU来执行同一个进程中的机器指令呢?

聪明的你应该能想到,既然我们可以把main函数的第一条指令地址写入PC寄存器,那么其它函数和main函数又有什么区别呢?

答案是没什么区别,main函数的特殊之处无非就在于是CPU执行的第一个函数,除此之外再无特别之处,我们可以把PC寄存器指向main函数,就可以把PC寄存器指向任何一个函数。

当我们把PC寄存器指向非main函数时,线程就诞生了。
在这里插入图片描述

至此我们解放了思想,一个进程内可以有多个入口函数,也就是说属于同一个进程中的机器指令可以被多个CPU同时执行。

注意,这是一个和进程不同的概念,创建进程时我们需要在内存中找到一块合适的区域以装入进程,然后把CPU的PC寄存器指向main函数,也就是说进程中只有一个执行流。
在这里插入图片描述
但是现在不一样了,多个CPU可以在同一个屋檐下(进程占用的内存区域)同时执行属于该进程的多个入口函数,也就是说现在一个进程内可以有多个执行流了。
在这里插入图片描述

总是叫执行流好像有点太容易理解了,再次祭出”弄不懂原则“,起个不容易懂的名字,就叫线程吧。

这就是线程的由来。

操作系统为每个进程维护了一堆信息,用来记录进程所处的内存空间等,这堆信息记为数据集A。

同样的,操作系统也需要为线程维护一堆信息,用来记录线程的入口函数或者栈信息等,这堆数据记为数据集B。

显然数据集B要比数据A的量要少,同时不像进程,创建一个线程时无需去内存中找一段内存空间,因为线程是运行在所处进程的地址空间的,这块地址空间在程序启动时已经创建完毕,同时线程是程序在运行期间创建的(进程启动后),因此当线程开始运行的时候这块地址空间就已经存在了,线程可以直接使用。这就是为什么各种教材上提的创建线程要比创建进程快的原因(当然还有其它原因)。

值得注意的是,有了线程这个概念后,我们只需要进程开启后创建多个线程就可以让所有CPU都忙起来,这就是所谓高性能、高并发的根本所在。
在这里插入图片描述
很简单,只需要创建出数量合适的线程就可以了。
另外值得注意的一点是,由于各个线程共享进程的内存地址空间,因此线程之间的通信无需借助操作系统,这给程序员带来极大方便的同时也带来了无尽的麻烦,多线程遇到的多数问题都出自于线程间通信简直太方便了以至于非常容易出错。

出错的根源在于CPU执行指令时根本没有线程的概念,多线程编程面临的互斥与同步问题需要程序员自己解决,关于互斥与同步问题限于篇幅就不详细展开了,大部分的操作系统资料都有详细讲解。

最后需要提醒的是,虽然前面关于线程讲解使用的图中用了多个CPU,但不是说一定要有多核才能使用多线程,在单核的情况下一样可以创建出多个线程,原因在于线程是操作系统层面的实现,和有多少个核心是没有关系的,CPU在执行机器指令时也意识不到执行的机器指令属于哪个线程。

即使在只有一个CPU的情况下,操作系统也可以通过线程调度让各个线程“同时”向前推进,方法就是将CPU的时间片在各个线程之间来回分配,这样多个线程看起来就是“同时”运行了,但实际上任意时刻还是只有一个线程在运行。

线程与内存

在前面的讨论中我们知道了线程和CPU的关系,也就是把CPU的PC寄存器指向线程的入口函数,这样线程就可以运行起来了,这就是为什么我们创建线程时必须指定一个入口函数的原因。无论使用任何编程语言,创建一个线程大体相同:

// 设置线程入口函数DoSomething
thread = CreateThread(DoSomething);

// 让线程运行起来
thread.Run();

那么线程和内存又有什么关联呢?

我们知道函数在被执行的时产生的数据包括函数参数、局部变量、返回地址等信息,这些信息是保存在栈中的,线程这个概念还没有出现时进程中只有一个执行流,因此只有一个栈,这个栈的栈底就是进程的入口函数,也就是main函数,假设main函数调用了funA,funcA又调用了funcB,如图所示:
在这里插入图片描述
那么有了线程以后了呢?

有了线程以后一个进程中就存在多个执行入口,即同时存在多个执行流,那么只有一个执行流的进程需要一个栈来保存运行时信息,那么很显然有多个执行流时就需要有多个栈来保存各个执行流的信息,也就是说操作系统要为每个线程在进程的地址空间中分配一个栈,即每个线程都有独属于自己的栈,能意识到这一点是极其关键的。
在这里插入图片描述
同时我们也可以看到,创建线程是要消耗进程内存空间的,这一点也值得注意。

线程任务类型

现在有了线程的概念,那么接下来作为程序员我们该如何使用线程呢?

从生命周期的角度讲,线程要处理的任务有两类:长任务和短任务。

  1. 长任务,long-lived tasks

    顾名思义,就是任务存活的时间很长,比如以我们常用的word为例,我们在word中编辑的文字需要保存在磁盘上,往磁盘上写数据就是一个任务,那么这时一个比较好的方法就是专门创建一个写磁盘的线程,该写线程的生命周期和word进程是一样的,只要打开word就要创建出该写线程,当用户关闭word时该线程才会被销毁,这就是长任务。

    这种场景非常适合创建专用的线程来处理某些特定任务,这种情况比较简单。
    有长任务,相应的就有短任务。

  2. 短任务,short-lived tasks

    这个概念也很简单,那就是任务的处理时间很短,比如一次网络请求、一次数据库查询等,这种任务可以在短时间内快速处理完成。因此短任务多见于各种Server,像web server、database server、file server、mail server等,这也是互联网行业的同学最常见的场景,这种场景是我们要重点讨论的。

    这种场景有两个特点:一个是任务处理所需时间短;另一个是任务数量巨大。

    如果让你来处理这种类型的任务该怎么办呢?

    你可能会想,这很简单啊,当server接收到一个请求后就创建一个线程来处理任务,处理完成后销毁该线程即可,So easy。
    这种方法通常被称为thread-per-request,也就是说来一个请求就创建一个线程:
    在这里插入图片描述

    如果是长任务,那么这种方法可以工作的很好,但是对于大量的短任务这种方法虽然实现简单但是有这样几个缺点:

    从前几节我们能看到,线程是操作系统中的概念(这里不讨论用户态线程实现、协程之类),因此创建线程天然需要借助操作系统来完成,操作系统创建和销毁线程是需要消耗时间的

    每个线程需要有自己独立的栈,因此当创建大量线程时会消耗过多的内存等系统资源

    这就好比你是一个工厂老板(想想都很开心有没有),手里有很多订单,每来一批订单就要招一批工人,生产的产品非常简单,工人们很快就能处理完,处理完这批订单后就把这些千辛万苦招过来的工人辞退掉,当有新的订单时你再千辛万苦的招一遍工人,干活儿5分钟招人10小时,如果你不是励志要让企业倒闭的话大概是不会这么做到的,因此一个更好的策略就是招一批人后就地养着,有订单时处理订单,没有订单时大家可以闲呆着。
    在这里插入图片描述
    这就是线程池的由来。

从多线程到线程池

线程池是创建一批线程,之后就不再释放了,有任务就提交给这些线程处理,因此无需频繁的创建、销毁线程,同时由于线程池中的线程个数通常是固定的,也不会消耗过多的内存,因此这里的思想就是复用、可控。

  1. 线程池是如何工作的

    该怎么给线程池提交任务呢?这些任务又是怎么给到线程池中线程呢?

    数据结构中的队列天然适合这种场景,提交任务的就是生产者,消费任务的线程就是消费者,实际上这就是经典的生产者-消费者问题。

    在这里插入图片描述

    一般来说提交给线程池的任务包含两部分:

    • 需要被处理的数据
    • 处理数据的函数

    struct task {
         
        void* data;     // 任务所携带的数据
        handler handle; // 处理数据的方法
    }
    

    线程池中的线程会阻塞在队列上,当生产者向队列中写入数据后,线程池中的某个线程会被唤醒,该线程从队列中取出上述结构体,以结构体中的数据为参数并调用处理函数:

    while(true) {
         
      struct task = GetFromQueue(); // 从队列中取出数据
      task->handle(task->data);     // 处理数据
    }
    

    以上就是线程池最核心的部分。
    理解这些你就能明白线程池是如何工作的了。

  2. 线程池中线程的数量

    现在线程池有了,那么线程池中线程的数量该是多少呢?

    要知道线程池的线程过少就不能充分利用CPU,线程创建的过多反而会造成系统性能下降,内存占用过多,线程切换造成的消耗等等。因此线程的数量既不能太多也不能太少,那到底该是多少呢?

    回答这个问题,你需要知道线程池处理的任务有哪几类,有的同学可能会说你不是说有两类吗?长任务和短任务,这个是从生命周期的角度来看的,那么从处理任务所需要的资源角度看也有两种类型:

    • CPU密集型
    • I/O密集型

    1. CPU密集型

      所谓CPU密集型就是说处理任务不需要依赖外部I/O,比如科学计算、矩阵运算等等。在这种情况下只要线程的数量和核数基本相同就可以充分利用CPU资源。
      在这里插入图片描述

    2. I/O密集型

      这一类任务可能计算部分所占用时间不多,大部分时间都用在了比如磁盘I/O、网络I/O等。
      在这里插入图片描述

    这种情况下就稍微复杂一些了,你需要利用性能测试工具评估出用在I/O等待上的时间,这里记为WT(wait time),以及CPU计算所需要的时间,这里记为CT(computing time),那么对于一个N核的系统,合适的线程数大概是N * (1 + WT/CT),假设I/O等待时间和计算时间相同,那么你大概需要2N个线程才能充分利用CPU资源,注意这只是一个理论值,具体设置多少需要根据真实的业务场景进行测试。

    当然充分利用CPU不是唯一需要考虑的点,随着线程数量的增多,内存占用、系统调度、打开的文件数量、打开的socker数量以及打开的数据库链接等等是都需要考虑的。

    因此这里没有万能公式,要具体情况具体分析。

    线程池不是万能的

    线程池仅仅是多线程的一种使用形式,因此多线程面临的问题线程池同样不能避免,像死锁问题、race condition问题等等,关于这一部分同样可以参考操作系统相关资料就能得到答案,所以基础很重要呀老铁们。

    线程池使用的最佳实践

    线程池是程序员手中强大的武器,互联网公司的各个server上几乎都能见到线程池的身影,使用线程池前你需要考虑:

    * 充分理解你的任务,是长任务还是短任务、是CPU密集型还是I/O密集型,如果两种都有,那么一种可能更好的办法是把这两类任务放到不同的线程池中,这样也许可以更好的确定线程数量

    * 如果线程池中的任务有I/O操作,那么务必对此任务设置超时,否则处理该任务的线程可能会一直阻塞下去

    * 线程池中的任务最好不要同步等待其它任务的结果

    话题总结

    我们从CPU开始一路来到常用的线程池,从底层到上层、从硬件到软件。注意,这里通篇没有出现任何特定的编程语言,线程不是语言层面的概念(依然不考虑用户态线程),但是当你真正理解了线程后,相信你可以在任何一门语言下用好多线程,你需要理解的是道,此后才是术。

实现线程

在Java中,实现线程的方式大体上分为三种,通过继承Thread类、实现Runnable接口,实现Callable接口。

  1. 继承Thread类

    public class ThreadDataPro{
         
    
        public static void main(String[] args) {
         
    
    		Ticket ticket=new Ticket();
    
    	    //创建四个线程对象
    	    Thread t1=new Thread(ticket,"窗口1");
    	    Thread t2=new Thread(ticket,"窗口2");
    	    Thread t3=new Thread(ticket,"窗口3");
    	    Thread t4=new Thread(ticket,"窗口4");
    	
    	    t1.start();
    	    t2.start();
    	    t3.start();
    	    t4.start();
        }
    
        static class Ticket extends Thread{
         
    
            private int tickets=10;
            //设置线程任务
            @Override
            public void run() {
         
    			//TODO 在此写在线程中执行的业务逻辑 
                while(true) {
         
                    if(tickets>0) {
         
                        String name=Thread.currentThread().getName();
    					//(由于i--不是原子操作(先获取i的值,让后再减一,再把结果赋给i),所以输出的值会有重复的情况,比如4 4 2)
                        System.out.println(name+"正在发售第"+tickets--+"张票");
                    }else {
         
                        break;
                    }
                }
            }
        }
    }
    

    在这里插入图片描述

  2. 实现Runnable接口

    实现Runnable接口的方法避免了使用Thread单继承的局限性,并且实现了解耦,任务可以被多个线程共享,任务和线程是独立的。

    public class RunnableTest implements Runnable {
         
    	 @Override public void run() {
         
    	  //TODO 在此写在线程中执行的业务逻辑 
    	  } 
      }
    
  3. 实现Callable接口的代码

    public class CallableTest implements Callable<String> {
         
    	 @Override public String call() throws Exception {
         
    	  //TODO 在此写在线程中执行的业务逻辑 return null; 
    	  } 
      }
    

线程的生命周期

一个线程从创建,到最终的消亡,需要经历多种不同的状态,即线程的生命周期。

根据Thread的内部类State中可得,线程可以处于以下状态之一:

在这里插入图片描述

• New(初始状态):线程被构建

• RUNNABLE(可运行状态) :调用start()方法进入可运行状态,内部包括 就绪状态运行中状态。它可能正在Java虚拟机中执行,也可能正在等待来自操作系统的其他资源,例如处理器。

• BLOCKED(休眠【阻塞】状态) :调用了阻塞API方法(synchronizd同步块),线程进入休眠状态。

• WAITING(休眠【等待】状态) :调用了阻塞API方法(wait()、join()、park()),线程进入休眠状态。需要等待其他线程唤醒

• TIME_WAITING(休眠【超时等待】状态) :WAITING状态的基础上加了超时时间,可以在一定的时间自行返回。

• TERMINATED(终止状态) :线程执行结束或者执行异常后线程终止

线程的执行顺序

调用Thread的start()方法启动线程时,线程的执行顺序是不确定的。
即在同一个方法中,连续创建多个线程后,调用线程的start()方法的顺序并不能决定线程的执行顺序。这里看一个简单的示例程序:

public class ThreadSort {
   
	public static void main(String[] args){
   
		Thread thread1 = new Thread(() -> {
   
			System.out.println("thread1"); 
		}); 
		Thread thread2 = new Thread(() -> {
   
			System.out.println("thread2"); 
		}); 
		Thread thread3 = new Thread(() -> {
   
			System.out.println("thread3"); 
		}); 
		thread1.start(); 
		thread2.start(); 
		thread3.start(); 
	} 
}

上述线程的执行顺序是否按照thread1、thread2和thread3的顺序执行呢?运行ThreadSort01的main方法,结果如下所示。

• thread1
• thread3
• thread2

多次运行可以看到,每次运行程序时,线程的执行顺序可能不同。线程的启动顺序并不能决定线程的执行顺序。

在实际业务场景中,有时,后启动的线程可能需要依赖先启动的线程执行完成才能正确的执行线程中的业务逻辑。此时,就需要确保线程的执行顺序。那么如何确保线程的执行顺序呢?

可以使用Thread类中的join()方法来确保线程的执行顺序:

public class ThreadSort02 {
   
    public static void main(String[] args) throws InterruptedException {
   
        Thread thread1 = new Thread(() -> {
   
            System.out.println("thread1");
        });
        Thread thread2 = new Thread(() -> {
   
            System.out.println("thread2");
        });
        Thread thread3 = new Thread(() -> {
   
            System.out.println("thread3");
        });
        thread1.start();  
        //实际上让主线程等待子线程执行完成
        thread1.join(); 
        thread2.start(); 
        thread2.join(); 
        thread3.start(); 
        thread3.join();
         }
}

结果如下:

• thread1
• thread2
• thread3

可以看到,每次运行的结果都是相同的,所以,使用Thread的join()方法能够保证线程的先后执行顺序。

查看源码分析:

–》调用同类中的一个有参join()方法

public final void join() throws InterruptedException {
   
   join(0);
}

–》被synchroinzed修饰,即同一时刻只能被一个实例或者方法调用。进入参数为0的代码逻辑

public final synchronized void join(long millis)
throws InterruptedException {
   
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
   
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
   
        while (isAlive()) {
   
            wait(0);
        }
    } else {
   
        while (isAlive()) {
   
            long delay = millis - now;
            if (delay <= 0) {
   
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

–》while循环判断当前线程是否处于活跃状态,如果已经启动处于活跃状态,则调用同类中的wait()方法,并传递参数0

if (millis == 0) {
   
    while (isAlive()) {
   
        wait(0); 
    }
}

–》本地方法wait()方法通过JNI的方式调用JDK底层的方法来使线程等待执行完成,这里会使主线程处于等待状态,等待子线程执行完成后再次向下执行。

–》即调用子线程的join()方法,会阻塞main()方法的执行,当子线程执行完成后,main()方法会继续向下执行,启动第二个子线程,并执行子线程的业务逻辑

public final native void wait(long timeout) throws InterruptedException;

线程的方法

方法 内容
void start() 启动一个线程
void run() 定义具体要执行的任务
void sleep(long millis) 让线程睡眠,在其睡眠时间内,该线程不会获得执行机会 ,若持有锁也不会释放锁
void sleep(long millis, int nanos)
yield() 让当前正在执行的线程暂停,转入到就绪状态,若持有锁也不会释放锁 (不会阻塞该线程)
void join() 等待thread线程执行完毕,可以控制线程执行顺序
void join(long millis) 等待thread线程一定的时间,可以控制线程执行顺序
void join(long millis, int nanos)
void interrupt() 将线程的中断状态设置会true,但是并没有立刻终止线程
boolean interrupted() 判断当前线程的中断状态(清除中断标志)
boolean isInterrupted() 判断调用者线程的中断状态(不清除中断标志)
void stop() 废弃的方法,它是一个不安全的方法
void destroy() 废弃的方法

线程的安全

多个线程访问该类的某个数据时,若不提供数据保护,可能出现线程先后更改数据造成所得到的数据是脏数据的问题

此时需要确保线程间的安全使用,采用加锁机制进行保护,直到该线程读取完其他线程才可使用。

解决线程安全问题分为两个方面:

• 控制执行
• 内存可见

  1. 控制执行

    控制代码执行(顺序)以及是否可以并发执行,这里使用到关键字synchronized.

    synchronized会使当前对象中被synchronized关键字保护的代码块无法被其他线程访问,也就无法并发执行。

    更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有cpu操作结果都会直接刷到主内存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作都happens-before于随后获得这个锁的线程的操作。

    实现方式有:

    • 同步代码块
    • 同步方法
    • lock锁机制

    由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。

    另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

  2. 内存可见性

    控制线程执行结果在内存中对其他线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(cpu缓存),操作完成后再把结果从线程本地刷到主存,这里使用到关键字volatile.

    volatile会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求,而对读取顺序没有要求的需求。

    使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意,volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

    在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,

线程的停止

• 线程执行完后自动终止
• stop强制终止(不安全)
• 使用interrupt方法

  1. interrupt方法

    interrupt方法将线程的中断状态设置会true,但是并没有立刻终止线程,就像告诉你儿子要好好学习一样,但是你儿子怎么样关键看的是你儿子。

    public static void main(String[] args) {
         
    
        try {
         
            MyThread myThread = new MyThread();
    
            Thread t=new Thread(myThread,"窗口");
            t.start();
            Thread.sleep(2);
            t.interrupt();
    
            System.out.println("是否停止1?="+myThread.interrupted());//false
            System.out.println("是否停止2?="+myThread.interrupted());//false main线程没有被中断
    
        } catch (InterruptedException e) {
         
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
    
    static class MyThread extends Thread {
         
        @Override
        public void run() {
         
            super.run();
            for (int i = 0; i < 500; i++) {
         
                System.out.println("i=" + (i + 1));
            }
        }
    }
    

    在这里插入图片描述

    上述例子thread线程执行了interrupt方法,判断中断状态为false,是因为interrupted方法 判断当前线程的中断状态(清除中断标志),也就是执行thread.interrupted方法的main线程,所以他的中断状态是false。

    以下是判断中断正确使用方式:

    public class Run {
         
        public static void main(String[] args) {
         
            try {
         
                Thread.currentThread().interrupt();
                System.out.println("是否停止1?="+Thread.interrupted());//true
                System.out.println("是否停止2?="+Thread.interrupted());//false 
          //......	
    

    也可以使用isInterrupted方法 判断调用者线程的中断状态(不清除中断标志)

    public static void main(String[] args) {
         
    		MyThread thread=new MyThread();
    		thread.start();
    		thread.interrupt();
    		System.out.println(thread.isInterrupted());//true
    		System.out.println(thread.isInterrupted());//true
    }
    

Callable接口

使用继承Thread类或者实现Runnable接口的线程,无法返回最终的执行结果数据,只能等待线程执行完成。

如果想要获取线程执行后的返回结果需要使用Callable接口、Future接口。

(JDK1.5)Callable函数式接口,内部声明了一个call方法并带有返回值,通过它可以在任务执行完毕之后得到任务执行结果

@FunctionalInterface
public interface Callable<V> {
   
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a re
  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值