周五(2019年4月12)项目上线时,有同事在面试别人,我听到他问:你了解多线程吗?能说一说吗?因此我意识到在面试当中多线程被问到的概率很大,结合自己2017年实习时,面试官同样也问多线程相关的基础知识。以前实习(2017年10月)找工作面试也准备过相关的知识点,今天我们再来温习一下多线程的一些知识,对多线程做一个总结和回顾,希望对多线程有更深刻的理解。
进程和线程 我们在深入了解多线程之前,我们口述一下进程和线程的概念,举例说明。
注意:多线程是异步的,所以千万不要把Eclipse里的代码的顺序当成线程执行的顺序,线程被调用的时机是随机的。
线程的组成 任何一个线程都有基本的组成部分。
CPU时间片:操作系统(OS)会为每个线程分配执行时间。
运行数据:
1.堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
2.栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
3.线程的逻辑代码
堆空间(对于java来说最重要,):存储各种各样的对象 被多线程共享 两个线程1,2 都创建一个对象,都放在堆空间中。
栈空间:存储的局部变量,线程独立,每个线程有自己的栈空间
方法区:常量池、静态属性等
Runnable:任务对象,只是给一个线程分配的一个代码(任务),不是线程
线程的创建和启动
public class TestThread {
public static void main(String[] args) {
//只是给线程分配一个任务task1
Runnable task1 = new Task1();
//只是给线程分配一个任务task2
Runnable task2 = new Task2();
//线程对象,虽然创建了线程对象thread1,thread2,但是操作系统底层并没有创建线程
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
//真正的线程才算启动
thread1.start();
thread2.start();
}
}
public class Task1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
System.out.println("***" + i);
}
}
}
public class Task2 implements Runnable{
@Override
public void run() {
for( int i = 0; i <= 1000; i++){
System.out.println("&&&" + i);
}
}
}
复制代码
以上总共3个线程,main方法主线程,thread1,thread2。
Thread:在java中表示线程的对象,线程对象来自虚拟机的堆空间
注意 : 线程对象和线程不一样 线程对象是java的对象 线程是操作系统的概念
线程的创建和状态
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建、就绪、运行、阻塞、终止。线程状态的转移和方法之间的关系如下图:
一个进程正常运行时至少会有一个线程在运行,这种情况在java中也是存在的。线程的调用是随机的
为什么线程启动之后没有马上执行,因为让他执行的线程正在运行 所以线程只能处于就绪状态。
runable状态:Ready Running状态(图)
主函数(main)执行完,说明主线程结束,进入终止状态 ; thread1,thread2就绪这时候CPU空暇,这时候操作系统就要从就绪状态(thread1,thread2)中挑选,挑选哪个由操作系统决定,此外,java是一个跨平台的语言,编译之后可能运行在任意一个操作系统上,不同的操作系统可能有不同的调度策略。(是不是一定挑选thread1,thread2不一定) window 按线程的优先级调度,而有些系统是共享式系统。
当一个进程所有线程终止状态,虚拟机进程(程序)才真正结束。
设置线程优先级
很粗略的方法:setPriority();
public class TestThread {
public static void main(String[] args) {
//只是给线程分配一个任务task1
Runnable task1 = new Task1();
//只是给线程分配一个任务task2
Runnable task2 = new Task2();
//线程对象,虽然创建了线程对象thread1,thread2,但是操作系统底层并没有创建线程
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
//设置线程优先级
thread1.setPriority(10);
//真正的线程才算启动
thread1.start();
thread2.start();
}
}
复制代码
线程的等待
yield方法:
放弃当前的CPU资源,将它让给其它任务去占用CPU执行时间。但是放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
在操作系统中,线程可以划分优先级,优先级较高的线程得到CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。设置线程的优先级有助于帮“线程规划器“确定在下一次选择哪一个线程来优先执行。 设置线程的优先级使用setPriotity()方法
概念:在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,CPU优先执行优先级较高的线程对象中的任务。
线程的基本方法
currentThread()方法 返回代码段正在被哪个线程调用的信息。
isAlive()方法 判断当前的线程是否处于活动状态
什么是活跃状态:线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是存活的
sleep()方法: 指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。这个正在执行的线程是指this.currentThread()返回的线程。
停止线程
Thread.stop()方法可以停止线程,但最好不用它。线程不安全,而且已经作废。
Thread.interrupt()方法:不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
在java中有以下3种方法可以终止正在运行的线程
1.使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2.使用stop方法强行终止线程,但是不推荐,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
3.使用interrupt()中断线程。
调用interrupt()方法仅仅是在当前线程中打了一个停止的标记并不是真的停止线程。
判断线程是否是停止状态
this.interrupted:测试当前线程是否已经中断(main方法) this.isInterrupted():测试线程是否已经中断
建议使用“抛异常“法实现线程的停止,因为在catch块中可以对异常的信息进行相关的处理,而且使用异常流更好、更方便地控制程序的运行流程,不至于代码中出现很多return;造成污染。
使用suspend与resume方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。
休眠:
public static void sleep(long millis)
当前线程主动休眠millis秒
放弃:
public staic void yield()
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
结合:
public final void join()
允许其他线程加入到当前线程中。
复制代码
限期等待
无限期等待
这个线程永远不会结束 但是其他线程一旦结束,虚拟机就终止(保护线程)
总结:
线程:一个程序顺序执行流程
并发原理:CPU时间分片,交替执行 宏观并行,微观串行。
数据空间:堆空间共享,栈空间独立
代码:实现Runnable接口,实现run方法, Runnable对象:任务对象 new Thread(任务对象) ,继承Thread类,覆盖run方法。
Thread.sleep() :限时等待 休眠
Thread.yield() :放弃CPU,回到就绪状态
setDaemon(true):设置线程为守护线程,所有的非守护线程都结束时,进程就好结束。
thread1.join():当前线程进入等待状态,知道tread1线程终止,才会恢复执行。
线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性.
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
-
间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。
-
直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理,那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就 是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
线程安全相关的问题
1.实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在线程之间的交互是很重要的一个技术点。
不共享数据的情况:
运行结果:
由 A计算,count4
由 A计算,count3
由 B计算,count4
由 B计算,count3
由 B计算,count2
由 B计算,count1
由 B计算,count0
由 A计算,count2
由 A计算,count1
由 A计算,count0
由 C计算,count4
由 C计算,count3
由 C计算,count2
由 C计算,count1
由 C计算,count0
复制代码
一共创建3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个变量。
如果想实现3个线程共同对一个count变量进行减法操作的目的,该如何设计代码呢?
共享数据的情况-多个线程可以访问同一个变量
比如实现投票功能的软件,多个线程可以同时处理同一个人的票数
运行结果:
由A计算count3
由D计算count1
由C计算count2
由B计算count3
由E计算count0
复制代码
从运行结果可以看到:线程A和B打印出的count值都是3,说明A和B同时对count进行处理,产生了"非线程安全"问题。而我们想要得到的结果却不是重复,那怎么办呢?
1.在某些JVM中,i--的操作要分成如下3步:
2.取得原有i值
3.计算i-1
4.对i进行赋值
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。 更改后的代码:
典型的销售场景:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减1操作,这时就需要使多个线程之间进行同步(用按顺序排队的方式进行减1操作)
通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。实现了排队调用run方法的目的,达到按顺序对count变量减1的效果。
synchronized可以在任何对象及方法上加锁,而加锁的这段代码称为互斥区或临界区。
非线程安全:
多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
LoginServlet.java
ALogin.java
BLogin.java
运行结果:
username=b====passwrod=bb
username=b====passwrod=aa
复制代码
更改后的代码
线程的局部变量
ThreadLocal 的作用和目的:
用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局 ThreadLocal对象的set方法,在set方法中,首先根据当前线程获取当前线程的ThreadLocalMap对象,然后往这个map中插入一条记录, key 其实是 ThreadLocal对象,value是各自的set方法传进去的值。也就是每个线程其实都有一份自己独享的ThreadLocalMap对象,该对象的Key是ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
ThreadLocal 的应用场景:
1.订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码 分别位于不同的模块类中。
2.银行转账包含一系列操作:把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在 同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同 的帐户对象的方法。
在Util类中创建ThreadLocal
发现项目中多线程
同步和异步的区别
同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。 区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
感谢博主分享:
全局搜索接口-异步记录热词(开启一个线程去创建热词):全局搜索接口查询 与记录热词没关系,,全局搜索接口响应不需要等待记录热词,,业务分离(发异步消息) 。
对于系统来说,一般很少去自己创建线程,业务性代码,创建线程的话需要去监控线程的,不一定能用好线程。
/**
* 异步(创建线程)记录热词
*
* @param keyword
* @param userId
*/
private void hotwordRecord(final String keyword, final String userId) {
if (StringUtils.isNotEmpty(keyword)) {
try {
Runnable hotwordRecord = new Runnable() {
@Override
public void run() {
hotwordService.createHotword(keyword, userId);
}
};
new Thread(hotwordRecord).start();
} catch (Exception e) {
logger.warn("global search hotword error! keyword is " + keyword, e);
}
}
}
复制代码