Java 线程相关
- 如何创建线程(两种方式,区别,使用场景)
- 线程状态调度
- 多线程数据共享(会有什么问题,如何实现共享,多线程操作同一个变量会有什么问题,如果不希望有问题怎么做)
- 数据传递
- 线程池相关(如何创建线程池,要注意什么(初始化线程内部变量),几种常用的使用方式)
1. 线程创建
通常创建线程有两种方式,一个是继承
Thread
, 一个是实现Runnable
; 下面则分别实现以做演示,然后说一下这两种的区别,应该如何选择
创建线程
创建线程和使用的一个小case如下, 注意的是线程启动是调用start
方法, 而不是 run
方法; 其次实现Runnable
接口的类,启动依然是放在一个Thread
对象中
public class ThreadCreate {
/**
* 通过继承 Thread 方式来创建一个新的线程
*/
public static class ThreadExtend extends Thread {
@Test
public void run() {
System.out.println("new extend thread");
}
}
/**
* 通过实现 Runnable 方式来创建一个线程
*/
public static class RunnableImplement implements Runnable {
@Override
public void run() {
System.out.println("new runnable thread");
}
}
@Test
public void testCreate() {
new ThreadExtend().start();
new Thread(new RunnableImplement()).start();
System.out.println("main!");
}
}
两种方式对比
为什么会有两种方式呢?这两种的区别何在?
- 实现是可以有多个的,但是继承只能有一个父类
- 查看 Runnable 的使用方法,最终是放在一个 Thread里面去执行的,所以在多个相同的程序代码处理一个资源时,这个还是有优势的;但是查看 Thread实际上就是 Runnable的实现,同样可以将一个自定义的Thread对象,创建多个 Thread对象来调用
通过上面的描述可以知道一点,如果你希望数据多线程内共享,不妨考虑实现 Runnable
接口(当然继承Thread也是ok的);如果希望隔离,则不妨考虑继承Thread
(实际上使用 Runnable接口的实现也是ok的,多创建几个实现类接口对象而已,每个对象放在一个新的Thread中执行)
按照个人的理解,网上说的实现Runnable
方便资源共享,更多的是倾向于代码的共享,通常是一个Runnable
对象,放在多个 Thread
实例中执行;而继承 Thead
类,从出发点来看,继承的一般是作为一个独立线程来执行使用,如果你真要像下面这么做,也不会报错,也能正常运行,只是有点违反设计理念而已
MyThread extreds Thread {...};
MyThread mythread = new MyThread();
new Thread(mythread).start();
case 举例
举一个例子,车站卖票,假设现在有三个窗口,总共只有30张车票,卖完就不卖了,怎么实现?如果每个窗口有10张车票,各个窗口把自己的卖完了就不卖了,怎么实现?
第一个case,符合数据共享的一种场景,那么我们的实现可以如下:
public static class TotalSaleTick implements Runnable {
private int total = 30;
@Override
public void run() {
while (true) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一张,剩余:" + --total);
} else {
break;
}
}
}
}
@Test
public void testTotalSale() {
TotalSaleTick totalSaleTick = new TotalSaleTick();
Thread thread1 = new Thread(totalSaleTick, "窗口1");
Thread thread2 = new Thread(totalSaleTick, "窗口2");
Thread thread3 = new Thread(totalSaleTick, "窗口3");
thread1.start();
thread2.start();
thread3.start();
System.out.println("master over!");
}
输出如下, 基本上每次跑的输出结果都不一样, 可以看出的一点是三个窗口售出的票数不同,一个问题,上面这种情况,可能造成超卖么?
窗口1售出一张,剩余:29
master over!
窗口2售出一张,剩余:28
窗口2售出一张,剩余:25
窗口2售出一张,剩余:24
窗口1售出一张,剩余:27
窗口3售出一张,剩余:26
窗口1售出一张,剩余:22
窗口1售出一张,剩余:20
窗口1售出一张,剩余:19
窗口1售出一张,剩余:18
窗口1售出一张,剩余:17
窗口1售出一张,剩余:16
窗口1售出一张,剩余:15
窗口1售出一张,剩余:14
窗口1售出一张,剩余:13
窗口1售出一张,剩余:12
窗口1售出一张,剩余:11
窗口1售出一张,剩余:10
窗口2售出一张,剩余:23
窗口2售出一张,剩余:8
窗口2售出一张,剩余:7
窗口2售出一张,剩余:6
窗口2售出一张,剩余:5
窗口2售出一张,剩余:4
窗口1售出一张,剩余:9
窗口3售出一张,剩余:21
窗口3售出一张,剩余:1
窗口3售出一张,剩余:0
窗口1售出一张,剩余:2
窗口2售出一张,剩余:3
第二个case,则显然更倾向于继承 Thread
来实现了
public static class SplitSaleTick extends Thread {
private int total = 10;
public SplitSaleTick(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一张,剩余:" + --total);
} else {
break;
}
}
}
}
@Test
public void testSplitSaleTick() {
SplitSaleTick splitSaleTick1 = new SplitSaleTick("窗口1");
SplitSaleTick splitSaleTick2 = new SplitSaleTick("窗口2");
SplitSaleTick splitSaleTick3 = new SplitSaleTick("窗口3");
splitSaleTick1.start();
splitSaleTick2.start();
splitSaleTick3.start();
System.out.println("master over");
}
/**
* 继承 Thread 也可以实现共享, 只不过比较恶心而已
*/
@Test
public void testSplitSaleTick2() {
SplitSaleTick splitSaleTick1 = new SplitSaleTick("saleTick");
Thread thread1 = new Thread(splitSaleTick1, "窗口1");
Thread thread2 = new Thread(splitSaleTick1, "窗口2");
Thread thread3 = new Thread(splitSaleTick1, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
输出接过如下, 三个窗口可以并发卖,且每个窗口卖10张,卖完即止
窗口1售出一张,剩余:9
窗口2售出一张,剩余:9
窗口2售出一张,剩余:8
窗口1售出一张,剩余:8
窗口1售出一张,剩余:7
窗口1售出一张,剩余:6
窗口1售出一张,剩余:5
窗口1售出一张,剩余:4
窗口2售出一张,剩余:7
窗口1售出一张,剩余:3
窗口1售出一张,剩余:2
窗口1售出一张,剩余:1
窗口1售出一张,剩余:0
窗口3售出一张,剩余:9
窗口3售出一张,剩余:8
窗口3售出一张,剩余:7
窗口3售出一张,剩余:6
窗口3售出一张,剩余:5
窗口3售出一张,剩余:4
窗口3售出一张,剩余:3
窗口3售出一张,剩余:2
窗口3售出一张,剩余:1
窗口3售出一张,剩余:0
master over
窗口2售出一张,剩余:6
窗口2售出一张,剩余:5
窗口2售出一张,剩余:4
窗口2售出一张,剩余:3
窗口2售出一张,剩余:2
窗口2售出一张,剩余:1
窗口2售出一张,剩余:0
---- test2 输出 ----
窗口1售出一张,剩余:9
窗口1售出一张,剩余:6
窗口1售出一张,剩余:5
窗口1售出一张,剩余:4
窗口1售出一张,剩余:3
窗口1售出一张,剩余:2
窗口3售出一张,剩余:7
窗口2售出一张,剩余:8
窗口3售出一张,剩余:0
窗口1售出一张,剩余:1
2. 线程状态(线程生命周期)
线程创建之后,即调用了start方法之后,线程是否开始运行了?这个运行过程是否会暂停呢?如果需要暂停应该怎么办;如果一个线程依赖另一个线程的计算结果,又该如何处理?
- 创建:新建一个线程对象,如
Thread thd=new Thread()
- 就绪:创建了线程对象后,调用了线程的
start()
方法(此时线程知识进入了线程队列,等待获取CPU服务 ,具备了运行的条件,但并不一定已经开始运行了) - 运行:处于就绪状态的线程,一旦获取了CPU资源,便进入到运行状态,开始执行run()方法里面的逻辑
- 终止:线程的
run()
方法执行完毕,或者线程调用了stop()
方法,线程便进入终止状态 - 阻塞:一个正在执行的线程在某些情况系,由于某种原因而暂时让出了CPU资源,暂停了自己的执行,便进入了阻塞状态,如调用了
sleep()
方法 - 线程让步:
join
等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态
3. 方法说明
一个Thread实例有一些常用的方法如:
start
,sleep
,run
,yield
,join
,wait
等, 这些方法是干嘛用的,什么场景下使用,使用时需要注意些什么?方法的执行,将对应线程状态进行说明
run 方法
run 方法中为具体的线程执行的代码逻辑,一般而言,都不应该被直接进行调用
无论我们采用哪种方法创建线程,基本上都是要重写run
方法,这个方法会在线程执行时调用
start 方法
执行该方法之后,线程进入就绪状态,对使用者而言,希望线程执行就是调用的这个方法(注意调用之后不会立即执行)
这个方法的主要目的就是告诉系统,我们的线程准备好了,cpu有空了赶紧来执行我们的线程
sleep 方法
睡眠一段时间,这个过程中不会释放线程持有的锁, 传入int类型的参数,表示睡眠多少ms
让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会
我们最常见的一种使用方式是在主线程中直接调用 Thread.sleep(100)
, 表示先等个100ms, 然后再继续执行
wait 方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程
通常我们执行wait方法是因为当前线程的执行,可能依赖到其他线程,如登录线程中,若发现用户没有注册,则等待,等用户注册成功后继续走登录流程(我们不考虑这个逻辑是否符合实际),
这里就可以在登录线程中调用 wait方法, 在注册线程中,在执行完毕之后,调用notify方法通知登录线程,注册完毕,然后继续进行登录后续action
yield 方法
暂停当前正在执行的线程对象,并执行其他线程
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
这个方法的执行,有点像一个拿到面包的人对另外几个人说,我把面包放在桌上,我们从新开始抢,那么下一个拿到面包的还是这些人中的某个(大家机会均等)
想象不出啥时候会这么干
join 方法
启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行
从上面的描述也可以很容易看出什么场景需要调用这个方法,主线程和子线程谁先结束不好说,如果主线程提前结束了,导致整个应用都关了,这个时候子线程没执行完,就呵呵了;其次就是子线程执行一系列计算,主线程会用到计算结果,那么就可以执行这个方法,保证子线程执行完毕后再使用计算结果
4. 数据共享
多线程间数据共享,当多线程公用一个Runnable对象时,这个对象中的成员变量即可以达到数据共享的目的;多线程采用不同的Runnable对象时,数据怎么共享
公用 Runnable
对象时
上面的售票例子中,其实就有这个场景,上面提出了一个问题,是否会出现超卖的情况?
-
因为我们知道
++
不是原子操作, 实际可以拆分为三步:- 内存到寄存器
- 寄存器自增
- 写回内存
假设num为10时, 线程A和线程B都调用 ++num操作;对于内存到寄存器这一步,两个线程都到了这一步,A自增将11写回内存,B也进行自增将11写会内存,这个时候就少+1了
读一个long,double类型的共享变量时,也不是原子操作,在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据,如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据
在多线程中,共享数据的获取or更新,请确保是原子操作;可以考虑同步锁(synchronized
)修改共享变量,共享变量前添加volatile
, 使用原子数据类型 AtomicInteger
修改上面的售票代码如下
public static class TotalSaleTick implements Runnable {
private int total = 30;
@Override
public void run() {
while (true) {
synchronized (this) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一张,剩余:" + --total);
} else {
break;
}
}
}
}
}
一个小疑惑,在实际的测试中,即便是上面不加上同步块,好像也没有出问题,对于上面的操作可能运行很多遍都是正确的, 好像和我们预期的不相符,有没有可能是因为总数太少,导致冲突的机率变小了?
private AtomicInteger count = new AtomicInteger(0);
private int sum = 3000;
public class MyThread extends Thread {
public void run() {
while (true) {
if (sum > 0) {
count.addAndGet(1);
--sum;
}else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + sum);
}
}
@Test
public void testAdd() throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
myThread1.join();
myThread2.join();
System.out.println("num: " + sum + " count: " + count.get());
}
对上面的场景,多运行几次,发现输出结果果然是超卖了
Thread-1 over -1
Thread-0 over -1
num: -1 count: 3008
非公用的 Runnable
对象时
共享全局变量 + 共享局部变量两种情况,有点区别
上面的case就是一个共享全局变量的demo,上面出现了并发冲突,可以如下解决, 针对类进行加锁
public class ThreadShareTest {
private AtomicInteger count = new AtomicInteger(0);
private int sum = 3000;
public class MyThread extends Thread {
public void run() {
while (true) {
if (sum > 0) {
synchronized (ThreadShareTest.class) {
if (sum > 0) {
count.addAndGet(1);
--sum;
}
}
}else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + sum);
}
}
@Test
public void testAdd() throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
myThread1.join();
myThread2.join();
System.out.println("num: " + sum + " count: " + count.get());
}
}
共享局部变量,需要注意的是局部变量要求是final, 所以下面的int采用了数组的形式(基本类型无法修改,引用类型可以改其内部的值, 不能改引用)
@Test
public void testAdd2() throws InterruptedException {
final int[] num = {3000};
final AtomicInteger c = new AtomicInteger(0);
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
if (num[0] > 0) {
c.addAndGet(1);
num[0]--;
} else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + num[0]);
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("num: " + num[0] + " count: " + c.get());
}
多运行几次,输出如下,说明也存在并发的问题了, 修正方式同样是加锁
Thread-0 over -1
Thread-1 over -1
num: -1 count: 3001
修改后的run方法内部如下
while (true) {
if (num[0] > 0) {
synchronized (this) {
if (num[0] > 0) {
c.addAndGet(1);
num[0]--;
} else {
break;
}
}
} else {
break;
}
}
线程数据隔离
上面是数据在多线程中共享,很容易出现的就是并发问题;还有一个场景就是我希望不存在数据共享,线程操作的内部变量不影响其他的线程; 最简单的想法就是一个继承了Thread的类,其内部类正常来讲就是隔离的,只要你不把它当成
Runnable
接口的使用方式就行
使用 ThreadLocal
来保证变量在线程之间的隔离, 下面是一个简单的演示,两个线程都是在修改threadLocal中的值, 但是两个线程的修改,对彼此而言是独立的
public static class LocalT implements Runnable {
ThreadLocal threadLocal = new ThreadLocal<>();
@Override
public void run() {
int start = (int) (Math.random() * 100);
for (int i =0 ; i < 100; i = i+2) {
threadLocal.set(start + i);
System.out.println(Thread.currentThread().getName() + " : " + get());
}
}
public int get() {
return threadLocal.get();
}
}
@Test
public void testLocal() throws InterruptedException {
LocalT local = new LocalT();
Thread thread1 = new Thread(local);
Thread thread2 = new Thread(local);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
5. 数据传递
数据如何传递给线程,有如何把线程计算的结果抛出来
传递数据
比较容易想到的就是在创建对象时,传入数据;或者调用线程对象的setXXX方法传入数据, 当做正常的对来操作处理即可
需要注意的是,在线程的执行期间,你修改了其中的局部变量,会出现什么情况呢?
public static class ThreadData implements Runnable {
private int num = 0;
public void run() {
while (num < 100) {
System.out.println(Thread.currentThread().getName() + " now: " + num++);
}
System.out.println(Thread.currentThread().getName() + " num: " + num);
}
public void setNum(int num) {
System.out.println(this.num + " now set to " + num);
this.num = num;
}
}
@Test
public void testThreadSetData() throws InterruptedException {
ThreadData threadData = new ThreadData();
Thread thread1 = new Thread(threadData);
Thread thread2 = new Thread(threadData);
thread1.start();
thread2.start();
threadData.setNum(200);
thread1.join();
thread1.join();
}
输出如下, 将num设置为200之后,并没有如我们预期的结束线程,依然在往下走, 这里就相当于是有一个你修改了这个数据,是否会立马就生效呢?特别是对其他的线程而言
...
Thread-1 now: 24
Thread-1 now: 25
Thread-0 now: 14
26 now set to 200
Thread-0 now: 27
Thread-0 now: 28
Thread-1 now: 26
Thread-0 now: 29
Thread-1 now: 30
....
输出结果
线程执行了一个任务之后,输出的结果可以怎么处理
一个实例,一个线程实现累加的过程,我现在希望实现1 加到 100, 开四个线程,怎么做?
下面是一个实现,不知道有没有什么问题
public static class CalculateThread extends Thread {
private int start;
private int end;
private int ans;
public CalculateThread(int start, int end) {
this.start = start;
this.end = end;
}
public void run() {
for (int i = start; i <= end; i++) {
ans += i;
}
}
public int getAns() {
return ans;
}
}
@Test
public void testCalculate() throws InterruptedException {
CalculateThread c1 = new CalculateThread(1, 25);
CalculateThread c2 = new CalculateThread(26, 50);
CalculateThread c3 = new CalculateThread(51, 75);
CalculateThread c4 = new CalculateThread(76, 100);
c1.start();
c2.start();
c3.start();
c4.start();
c1.join();
c2.join();
c3.join();
c4.join();
System.out.println("ans1: " + c1.getAns() + " ans2: " + c2.getAns() + " ans3: " + c3.getAns() + " ans4: " + c4.getAns());
int ans = c1.getAns() + c2.getAns() + c3.getAns() + c4.getAns();
System.out.println("ans : " + ans);
}
参考
- Java多线程操作局部变量与全局变量
- Java多线程学习(吐血超详细总结)