- 进程和线程的区别是什么?
- 创建线程的几种方式?
- 线程的生命周期和线程的几种状态?
- 同步方法和同步代码块的区别是什么 ?
- 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
- 什么是死锁(deadlock)?
- 如何确保N个线程可以访问N个资源同时又不导致死锁 ?
- 锁池和等待池?
- sleep()、wait()、join()、yield()的区别
- 什么叫 线程上下文切换?
- 如何减少 上下文切换?
- JUC常见方法
- 线程池
- 避免死锁的几个常见方法
- 对线程安全的理解
- Thread、Runable的区别
- 对守护线程Deamon的理解
- ThreadLocal的原理和使用场景
- ThreadLocal内存泄露原因,如何避免
- 并发、并行、串行的区别
- 并发的三大特性
- volatile
- 为什么用线程池?解释下线程池参数?
- 简述线程池处理流程
- 线程池中阻塞队列的作用?为什么是先添加列队而不是先
- 创建最大线程?
- 线程池中线程复用原理
进程和线程的区别是什么?
- 进程是运行中的程序,线程是进程的内部的一个执行序列(一个进程内部有多个线程,多个线程共享进程的资源)
- 进程是资源分配的单元,线程是执行行单元
- 进程间切换代价大,线程间切换代价小
- 进程拥有资源多,线程拥有资源少
对于这种题目,专业术语不好理解,采用形象一点的比喻更好,比如(取自知乎): 开个QQ,开了一个进程;开了迅雷,开了一个进程。 在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。 所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ 的运行,那么这“多个工作”分别有一个线程。
创建线程的几种方式?
1、继承 Thread类
方式1:
public MainTest{
public static void main(String[] args){
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println(super.getName()+":"+i);
}
}
}
方式2:
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("hello thread");
}
};
t1.start();
System.out.println(t1.getName());
System.out.println("当前主线程是:"+ Thread.currentThread().getName());
方式3:Lambda接口
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
R = 10;
},"t1");
t1.start();
t1.join();
log.debug(Thread.currentThread().getName());
2、使用 Runnable 配合 Thread
实现Runnable接口,重写run方法
run方法里写的是要执行的任务,该方法将【线程】和【任务】分开了
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 创建线程对象
Thread t = new Thread(runnable,"t1");
// 启动线程
t.start();*/
3、FutureTask 配合Thread
继承Thread和实现Runnable接口这两种创建线程的方法都没有返回值,FutureTask 方法可以有返回值
https://www.cnblogs.com/cjsblog/p/10905263.html
https://blog.csdn.net/qq_39654841/article/details/90631795
FutureTask实现了Future接口,Future接口有5个方法: 1、boolean cancel(boolean mayInterruptIfRunning) 尝试取消当前任务的执行。如果任务已经取消、已经完成或者其他原因不能取消,尝试将失败。如果任务还没有启动就调用了cancel(true),任务将永远不会被执行。如果任务已经启动,参数mayInterruptIfRunning将决定任务是否应该中断执行该任务的线程,以尝试中断该任务。 如果任务不能被取消,通常是因为它已经正常完成,此时返回false,否则返回true 2、boolean isCancelled() 如果任务在正常结束之前被被取消返回true 3、boolean isDone() 正常结束、异常或者被取消导致任务完成,将返回true 4、V get() 等待任务结束,然后获取结果,如果任务在等待过程中被终端将抛出InterruptedException,如果任务被取消将抛出CancellationException,如果任务中执行过程中发生异常将抛出ExecutionException。 5、V get(long timeout, TimeUnit unit) 任务最多在给定时间内完成并返回结果,如果没有在给定时间内完成任务将抛出TimeoutException。
FutureTask futureTask = new FutureTask<Integer>(() ->{
System.out.println("执行线程任务");
return 100;
});
try {
Thread t3 = new Thread(futureTask, "t3");
t3.start();
Object o = futureTask.get();
System.out.println(o);
} catch (ExecutionException e) {
e.printStackTrace();
}
打印结果:
执行线程任务
100
4、线程池
- 使用线程池
public static void test01(){
ExecutorService es = Executors.newFixedThreadPool(100);
es.submit(()->{
//start
System.out.println("开始执行线程--->"+Thread.currentThread().getName());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
线程的生命周期和线程的几种状态?
线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
阻塞的情况又分为三种:
(1)等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法,所有对象都可以调用;
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中;
(3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的静态方法;
同步方法和同步代码块的区别是什么 ?
在 Java 语言中,每一个对象有一把锁。线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。
数据安全问题出现的条件:
- 多线程环境
- 共享数据
- 有多条语句操作共享数据
在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
链接:https://www.nowcoder.com/questionTerminal/26fc16a2a85e49a5bd5fc2b5759dbbc2
来源:牛客网
在 java 虚拟机中, 每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁。一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案什么是死锁(deadlock)?
链接:https://www.nowcoder.com/questionTerminal/09b51b00891543d6b08ace80c0704b01
来源:牛客网
死锁 :是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
1、因为系统资源不足。
2、进程运行推进顺序不合适。
3、资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则 就会因争夺有限的资源而陷入死锁。其次,进程
运行推进顺序与速度不同,也可能产生死锁。如何确保N个线程可以访问N个资源同时又不导致死锁 ?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线
程都是以同样的顺序加锁和释放锁,就不会出现死锁了。锁池和等待池?
1、锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线
程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到
后会进入就绪队列进行等待cpu资源分配。
2、等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放
到锁池,而notifyAll()是将等待池的所有线程放到锁池当中sleep()、wait()、join()、yield()的区别
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放锁,而且会加入到等待队列中。sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu 的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而 是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程 序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出 interruptexception异常返回,这点和wait是一样的。
而wait会释放锁 交出CPU的执行权,进入到等待池中,直到调用notify或notifyAll后才会唤醒,进入就绪状态等待CPU调用;
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞
争到锁继续执行的。
7、yield()执行后线程直接从Running(运行状态)进入Runnable(就绪状态),会打断synchronize锁 让出CPU的执行权;
8、join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队
列,直到线程A结束或中断线程;
举例:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("22222222");
}
});
t1.start();
t1.join();
// 这行代码必须要等t1全部执行完毕,才会执行
System.out.println("1111");
}
输出:
22222222
1111
什么叫 线程上下文切换?
(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完;
- 垃圾回收;
- 有更高优先级的线程需要运行;
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法;
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的,
当前线程的状态包括:程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等;
如何减少 上下文切换?
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
JUC常见方法
主要是Thread.java类
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新线程中运行 run 方法中的代码 | start 方法只是让线程进入就绪状态,里面代码不一定立刻运行,只有当 CPU 将时间片分给线程时,才能进入运行状态,执行代码。每个线程的 start 方法只能调用一次,调用多次就会出现 IllegalThreadStateException | |
run() | 新线程启动会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id id 唯一 | ||
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数(默认是5),较大的优先级能提高该线程被 CPU 调度的机率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记,park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记(如果原来是true,调用该方法后先返回true 然后将打断标记改变一下变为false) |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | 会抛出异常 |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
State | public enum State { | 枚举 列出线程6种状态 |
构造方法:
重构了
public Thread( );
public Thread(Runnable target);
public Thread(String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Runnable target
实现了Runnable接口的类的实例。要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。
String name
线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名子,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。
ThreadGroup group
当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中(默认情况下,所有的线程都属于主线程组)
long stackSize
线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB.在x86平台下,默认的线程栈大小是12KB.
一个普通的java类只要从Thread类继承,就可以成为一个线程类。并可通过Thread类的start方法来执行线程代码。虽然Thread类的子类可以直接实例化,但在子类中必须要覆盖Thread类的run方法才能真正运行线程的代码。
原文链接:https://blog.csdn.net/liushijiao258/article/details/7941901
创建一个线程组,创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
Thread(ThreadGroup group, Runnable target, String name)
// ThreadGroup(String name)
ThreadGroup tg = new ThreadGroup("这是一个新的组");
MyRunnable my = new MyRunnable();
// Thread(ThreadGroup group, Runnable target, String name)
Thread t1 = new Thread(tg, my, "刘织忋");
Thread t2 = new Thread(tg, my, "马化腾");
System.out.println(t1.getThreadGroup().getName());
System.out.println(t2.getThreadGroup().getName());
//通过组名称设置后台线程,表示该组的线程都是后台线程
tg.setDaemon(true);
//可通过ThreadGroup方法统一操作整个线程组线程
线程池
线程池概述
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。
Executors工厂类
避免死锁的几个常见方法
举例出现死锁的情况:
锁是个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。让我们先来看一段代码,这段代码会引起死锁,使线程t1和线程t2互相等待对方释放锁。
public class DeadLockDemo {
privat static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为复杂的场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看到底是哪个线程出现了问题,以下线程信息告诉我们是DeadLockDemo类的第42行和第31行引起的死锁。
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
对线程安全的理解
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获 得正确的结果,我们就说这个对象是线程安全的
堆 是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分
配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了
要还给操作系统,要不然就是内存泄漏。
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚
拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及
数组都在这里分配内存。
栈 是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语 言里面显式的分配和释放。 目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己 的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以
访问到该区域,这就是造成线程安全问题的潜在原因。
Thread、Runable的区别
无论使用Runnable还是Thread,都会new
Thread,然后执行run方法。
用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简
单的执行一个任务,那就实现runnable
实现Runnable接口比继承Thread类所具有的优势:
1):把【线程】和【任务】(要执行的代码)分开
,Thread 代表线程
,Runnable 可运行的任务(线程要执行的代码)
2):可以避免java中的单继承的限制(继承Thread类实现run方法,实现Runnable接口实现重写run方法)
3):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简
单的执行一个任务,那就实现runnable。
对守护线程Deamon的理解
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true); 方法变成守护线程
注意 垃圾回收器线程就是一种守护线程, Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求,
守护线程的作用是什么?
举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就
不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线
程会自动结束。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
守护线程的应用:
(1)来为其它线程提供服务支持的情况;
(2)或者在任何情况下,程序结束时,这个线
程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要
正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都
是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
(3)thread.setDaemon(true)
必须在thread.start()之前设置,否则会抛出一个
IllegalThreadStateException异常。
(4)不能把正在运行的常规线程设置为守护线程。
(5)在Daemon线程中产生的新线程也是Daemon的。
(6)守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作
的中间发生中断。
(7)Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线
程就不能用Java的线程池。
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;