Java基础系列15: 多线程
本文是作者的读书笔记和心得整理,部分内容来源于网络,如有侵权,请联系作者。
什么是线程
线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。
实现多线程主要是继承Thread类和实现Runable接口
线程的生命周期
如下图,引用自:https://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg
- 新建状态:使用new关键字创建一个Thread或者其子类的对象之后,就处于这个状态,直到start()
- 就绪状态:start之后
- 运行状态:CPU调用之后执行run()方法
- 阻塞状态:执行了sleep或者suspend方法,可能是因为等待阻塞wait方法,或者是synchronized同步锁被占用,或者是sleep/join发出了阻塞请求
- 运行完毕之后就进入终止状态
多线程的实现
例子代码来源于:https://how2playlife.com/2019/09/17/17%E3%80%81%E5%A4%9A%E7%BA%BF%E7%A8%8B/
分别实现两个子类继承自Thread/实现Runnable接口,注意及时是继承了Runnable接口的类也要用Thread包装之后才能运行
//继承thread
@Test
public void test1() {
class A extends Thread {
@Override
public void run() {
System.out.println("A run");
}
}
A a = new A();
a.start();
}
//实现Runnable
@Test
public void test2() {
class B implements Runnable {
@Override
public void run() {
System.out.println("B run");
}
}
B b = new B();
//Runable实现类需要由Thread类包装后才能执行
new Thread(b).start();
}
//有返回值的线程
@Test
public void test3() {
Callable callable = new Callable() {
int sum = 0;
@Override
public Object call() throws Exception {
for (int i = 0;i < 5;i ++) {
sum += i;
}
return sum;
}
};
//这里要用FutureTask,否则不能加入Thread构造方法
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//线程池实现
@Test
public void test4() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
//execute直接执行线程
executorService.execute(new Thread());
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("runnable");
}
});
//submit提交有返回结果的任务,运行完后返回结果。
Future future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "a";
}
});
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
ArrayList<String> list = new ArrayList<>();
//有返回值的线程组将返回值存进集合
for (int i = 0;i < 5;i ++ ) {
int finalI = i;
Future future1 = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "res" + finalI;
}
});
try {
list.add((String) future1.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
for (String s : list) {
System.out.println(s);
}
}
这里主要是要提示一下使用callable,要先声明一个callable对象,再声明一个FutureTask对象,最后用Thread包装一下才能运行.
获取结果要用futureTake.get来获得
线程状态的切换
同样是直接上例子
public class 线程的状态转换 {
//一开始线程是init状态,结束时是terminated状态
class t implements Runnable {
private String name;
public t(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + "run");
}
}
//测试join,父线程在子线程运行时进入waiting状态
@Test
public void test1() throws InterruptedException {
Thread dad = new Thread(new Runnable() {
Thread son = new Thread(new t("son"));
@Override
public void run() {
System.out.println("dad init");
son.start();
try {
//保证子线程运行完再运行父线程,join非常重要
son.join();
System.out.println("dad run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//调用start,线程进入runnable状态,等待系统调度
dad.start();
//在父线程中对子线程实例使用join,保证子线程在父线程之前执行完
}
//测试sleep
@Test
public void test2(){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 run");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//主线程休眠。进入time waiting状态
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
}
//线程2进入blocked状态。
public static void main(String[] args) {
test4();
Thread.yield();//进入runnable状态
}
//测试blocked状态
public static void test4() {
class A {
//线程1获得实例锁以后线程2无法获得实例锁,所以进入blocked状态
synchronized void run() {
while (true) {
System.out.println("run");
}
}
}
A a = new A();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 get lock");
a.run();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t2 get lock");
a.run();
}
}).start();
}
//volatile保证线程可见性
volatile static int flag = 1;
//object作为锁对象,用于线程使用wait和notify方法
volatile static Object o = new Object();
//测试wait和notify
//wait后进入waiting状态,被notify进入blocked(阻塞等待锁释放)或者runnable状态(获取到锁)
public void test5() {
new Thread(new Runnable() {
@Override
public void run() {
//wait和notify只能在同步代码块内使用
synchronized (o) {
while (true) {
if (flag == 0) {
try {
Thread.sleep(2000);
System.out.println("thread1 wait");
//释放锁,线程挂起进入object的等待队列,后续代码运行
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1 run");
System.out.println("notify t2");
flag = 0;
//通知等待队列的一个线程获取锁
o.notify();
}
}
}
}).start();
//解释同上
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (o) {
if (flag == 1) {
try {
Thread.sleep(2000);
System.out.println("thread2 wait");
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 run");
System.out.println("notify t1");
flag = 1;
o.notify();
}
}
}
}).start();
}
//输出结果是
// thread1 run
// notify t2
// thread1 wait
// thread2 run
// notify t1
// thread2 wait
// thread1 run
// notify t2
//不断循环
}
Java Thread 常用方法
yield
放弃当前CPU的使用,但是保留当前运行情况
interrupt
中断当前线程执行,调用之后会清除当前线程的interrupt status
join
A线程调用B线程的join()方法,将会使A等待B执行,直到B线程终止。如果传入time参数,将会使A等待B执行time的时间,如果time时间到达,将会切换进A线程,继续执行A线程。
线程的构造方法
构造方法
Thread类中不同的构造方法接受如下参数的不同组合:
一个Runnable对象,这种情况下,Thread.start方法将会调用对应Runnable对象的run方法。如果没有提供Runnable对象,那么就会立即得到一个Thread.run的默认实现。
一个作为线程标识名的String字符串,该标识在跟踪和调试过程中会非常有用,除此别无它用。
线程组(ThreadGroup),用来放置新创建的线程,如果提供的ThreadGroup不允许被访问,那么就会抛出一个SecurityException 。
Thread对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性(通过setDaemon方法)。
当程序中所有的非守护线程都已经终止,调用setDaemon方法可能会导致虚拟机粗暴的终止线程并退出。
isDaemon方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作。
(daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,比如打印机队列管理,它总是在系统中运行。)
线程的启动
调用start方法会触发Thread实例启动一个线程并运行run方法.
当线程运行完之后则会终止.终止态的线程是无法被重新启动的.
可以调用isAlive方法来判断线程是否终止.
线程的优先权
引用自:https://how2playlife.com/2019/09/17/17%E3%80%81%E5%A4%9A%E7%BA%BF%E7%A8%8B/
Java的线程实现基本上都是内核级线程的实现,所以Java线程的具体执行还取决于操作系统的特性。
Java虚拟机为了实现跨平台(不同的硬件平台和各种操作系统)的特性,Java语言在线程调度与调度公平性上未作出任何的承诺,甚至都不会严格保证线程会被执行。但是Java线程却支持优先级的方法,这些方法会影响线程的调度:
每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)
默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).
线程的当前优先级可以通过getPriority方法获得。
线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。
经典多线程面经
1,volatile关键字的作用:
1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。
使用volatile有以下条件:
1、对变量的写操作不依赖当前变量的值;
2、该变量没有包含在其他变量的不变式中。
主要是因为只有可见性,但是没有原子性,用于读多写少的情况.
2,什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的
3,如何在线程之间共享数据
1,通过共享对象
2,使用阻塞队列BlockingQueue
4,wait和sleep的区别
wait会放弃对象监视器,sleep不会
5,ThreadLocal方法
ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了
6,高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。