(1)线程系列 - 线程、多线程
我们经常用的okhttp和rxjava等,都是基于线程进行封装,我们从java最基础上了解线程对于以后是有帮助的,那么直接进入主题
相关概念
在说线程之前,我们先了解一下进程。
什么是进程
我们平日里打开的微信、简书App,都是一个进程。
什么是线程
线程是比进程更小的执行单位。一个程序只可以有一个进程,但这个进程可以包含多个线程
什么是多线程
这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程
并发和并行
并发:并发是指一个处理器同时处理多个任务
并行:并行是指多个处理器或者是多核的处理器同时处理多个不同的任务
打比方:并发是一个人同时吃三个包子,而并行是三个人同时吃三个包子
什么是线程池
创建并销毁线程的过程势必会消耗内存,如果创建多个线程对于Java来说是不合适的,Java的内存资源是极其宝贵的,所有就有了这个线程池重复利用线程
1. 线程
自定义Thread
/**
* 自定义线程类
* @author zhongjh
* @date 2021/5/7
*/
public class MyThread extends Thread {
private static final int COUNT = 10;
/**
* 线程名称
*/
private final String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
System.out.println(threadName + ": " + i);
}
}
}
// 并行执行多个线程
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
myThread1.start();
myThread2.start();
打印日志
image.png
实现Runnable接口
/**
* @author zhongjh
* @date 2021/5/7
*/
public class MyThreadImpl implements Runnable {
private static final int COUNT = 10;
/**
* 线程名称
*/
private final String threadName;
public MyThreadImpl(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
System.out.println(threadName + ": " + i);
}
}
}
// 并行执行多个线程
MyThreadImpl myThreadImpl1 = new MyThreadImpl("线程1");
MyThreadImpl myThreadImpl2 = new MyThreadImpl("线程2");
Thread myThreadOne = new Thread(myThreadImpl1);
Thread myThreadTwo = new Thread(myThreadImpl2);
myThreadOne.start();
myThreadTwo.start();
打印日志的内容跟继承Thread是一样的,Thread的源码也是实现Runnable接口,两者区别就是接口跟继承的区别,在很多场景中接口比继承灵活多了,当然,这是接口继承的另一篇文章了。
线程流程
创建
new Thread()创建线程后,此时已经有了相应的内存空间和其他资源。
准备
调用线程的start()方法后,线程将进入线程队列排队,等待 CPU 服务,此时的线程已经具备了运行条件。
运行
当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时该线程自动它的 run() 方法。
阻塞
线程在运行过程中,如果人为调用sleep(),suspend(),wait() 等方法或者别的因素,线程将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
运行
线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程将不会有继续运行的能力。
定义线程名称
Thread.currentThread().getName(); // 取得当前线程的名称
也可以通过new Thread(Runnable, "线程1");这种方式自定义赋值名称
join
join方法的功能就是使异步执行的线程变成同步执行。
平常的调用线程实例的start方法后,这个方法会立即返回,如果后面的代码想得到这个线程返回的值才能计算,那么就必须使用join方法。
public class MyThreadJoin extends Thread {
int m = (int) (Math.random() * 10000);
@Override
public void run() {
try {
System.out.println("我在子线程中会随机睡上0-9秒,时间为="+m);
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* join方法示例
*/
private void testJoin() {
MyThreadJoin myThread =new MyThreadJoin();
myThread.start();
try {
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正常情况下肯定是我先执行完,但是加入join后,main主线程会等待子线程执行完毕后才执行");
}
打印日志,可以看到6秒后才显示
image.png
sleep
线程常用方法之一,sleep(xx毫秒),线程的休眠。顾名思义,暂停线程xx毫秒之后继续执行,直接看示例代码
public class MyThreadSleep extends Thread {
private static final int COUNT = 3;
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
/**
* Sleep示例
*/
private void testSleep() {
MyThreadSleep myThread1 = new MyThreadSleep();
myThread1.start();
}
打印日志,可以看到相隔1秒才显示一句日志
image.png
sleep还有一种写法,Thread.sleep(long millis),这是针对所有线程的睡眠
yield
yield()会礼让给相同优先级的或者是优先级更高的线程执行,yield()这个方法只是把线程的状态打回准备状态,他会继续跑起来,可以看到代码例子有个停住1秒的,可以尝试把1秒暂停看看打印出来的文字
/**
* Yield示例
*/
private void testYield() {
MyThreadYield myThread1 = new MyThreadYield("线程一");
MyThreadYield myThread2 = new MyThreadYield("线程二");
myThread1.start();
myThread2.start();
}
/**
* 礼让线程
* @author zhongjh
* @date 2021/5/14
*/
public class MyThreadYield extends Thread {
public MyThreadYield(String name) {
super(name);
}
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "在运行,i的值为:" + i + " 优先级为:" + getPriority());
if (i == 2) {
System.out.println(getName() + "礼让");
Thread.yield();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
打印文字
image.png
synchronized
在更深入的讲解线程其他机制前,我们先讲另一个关键字,synchronized。
这是一个同步关键字,不管多少个线程调用该关键字修饰的方法,都是一个一个的按照顺序执行完。假设我们多个线程(人)买火车票
private int ticket = 10;
/**
* Synchronized购买火车票的示例
*/
private void testSynchronized() {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
// 买票
sellTicket();
}
}.start();
}
}
/**
* 减少票,同步synchronized
*/
public synchronized void sellTicket() {
ticket--;
System.out.println("剩余的票数:" + ticket);
if (ticket == 0) {
// 重新填充票数用于测试
ticket = 10;
}
}
打印日志,可以看到票数顺序减少,如果去掉synchronized,可以发现乱序的
image.png
synchronized 对象锁和类锁
不同对象之间的对象锁是互不影响的,而类锁只有一个。但是同时对象锁和类锁又不互不影响,接着会通过代码分别加深锁的印象
首先创建一个实体类,包含了对象锁和类锁的方法
public class SynchronizedEntity {
private int ticket = 10;
/**
* 同步方法,对象锁
*/
public synchronized void syncMethod() {
for (int i = 0; i < 1000; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
}
/**
* 同步块,对象锁
*/
public void syncThis() {
synchronized (this) {
for (int i = 0; i < 1000; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
}
}
/**
* 同步class对象,类锁
*/
public void syncClassMethod() {
synchronized (SynchronizedEntity.class) {
for (int i = 0; i < 50; i++) {
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
}
}
/**
* 同步静态方法,类锁
*/
public static synchronized void syncStaticMethod(){
// 暂不演示该方法
}
}
多个线程调用同一个对象锁
/**
* 多个线程调用同一个对象锁
*/
private void testSynchronized2() {
final SynchronizedEntity synchronizedEntity = new SynchronizedEntity();
// 线程一
new Thread() {
@Override
public void run() {
synchronizedEntity.syncMethod();
}
}.start();
// 线程二
new Thread() {
@Override
public void run() {
synchronizedEntity.syncThis();
}
}.start();
}
打印日志可以看到有效的顺序执行
image.png
两个线程分别调用不同对象锁
/**
* 两个线程分别调用不同对象锁
*/
private void testSynchronized3() {
final SynchronizedEntity synchronizedEntity1 = new SynchronizedEntity();
final SynchronizedEntity synchronizedEntity2 = new SynchronizedEntity();
// 线程一
new Thread() {
@Override
public void run() {
synchronizedEntity1.syncMethod();
}
}.start();
// 线程二
new Thread() {
@Override
public void run() {
synchronizedEntity2.syncMethod();
}
}.start();
}
打印日志可以看到票数顺序乱了
image.png
两个线程分别调用对象锁、类锁
/**
* 两个线程分别调用对象锁、类锁
*/
private void testSynchronized4() {
final SynchronizedEntity synchronizedDemo = new SynchronizedEntity();
// 线程一
new Thread() {
@Override
public void run() {
synchronizedDemo.syncMethod();
}
}.start();
// 线程二
new Thread() {
@Override
public void run() {
synchronizedDemo.syncClassMethod();
}
}.start();
}
打印日志如图,他们互不影响,所以也是线程不安全的
总结:不同对象之间的对象锁是互不影响的,而类锁只有一个。但是同时对象锁和类锁又不互不影响
wait()、notify()、notifyAll()
wait、notify、notifyAll都必须在synchronized中执行,否则会抛出异常。所以为什么会先讲synchronized
notify跟notifyAll区别是notify会唤醒等待唤醒队列中
的第一个线程,而notifyAll()方法则是唤醒整个唤醒队列中
的所有线程
直接上代码,先创建一个线程,该线程是sleep自身2秒后再唤醒所有线程
public class MyThreadWait extends Thread {
private final Object lockObject;
public MyThreadWait(Object lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
synchronized (lockObject) {
try {
// 子线程等待了2秒钟后唤醒lockObject锁
sleep(2000);
System.out.println("lockObject唤醒");
lockObject.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* wait示例
*/
private void testWait() {
// 创建子线程
Thread thread = new MyThreadWait(lockObject);
thread.start();
long start = System.currentTimeMillis();
synchronized (lockObject) {
try {
System.out.println("lockObject等待");
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("lockObject继续 --> 等待的时间:" + (System.currentTimeMillis() - start));
}
}
wait()、notify()、notifyAll() 进阶
接着是java经典题目,子线程循环2次,接着主线程循环3次,接着又回到子线程循环2次,接着再回到主线程又循环3次,如此循环10次
/**
* 锁对象
*/
private final Object lock = new Object();
/**
* 是否执行子线程标志位
*/
boolean beShouldSub = true;
/**
* wait和notify示例
* 子线程循环2次,接着主线程循环3次,接着又回到子线程循环2次,接着再回到主线程又循环3次,如此循环10次
*/
private void testWaitNotify() {
// 子线程
new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
testWaitNotifyThread();
}
}
}.start();
// 主线程
for (int i = 0; i < 10; i++) {
testWaitNotifyMain();
}
}
/**
* 子线程循环两次
*/
private void testWaitNotifyThread() {
synchronized (lock) {
if (!beShouldSub) {
// 等待
try {
Log.d("testWaitNotify","子线程等待lock");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 0; j < 2; j++) {
Log.d("testWaitNotify","子循环第" + (j + 1) + "次");
}
// 子线程执行完毕,子线程标志位设为false
beShouldSub = false;
// 唤醒
Log.d("testWaitNotify","子线程唤醒lock");
lock.notify();
}
}
/**
* 主线程循环3次
*/
private void testWaitNotifyMain() {
synchronized (lock) {
if (beShouldSub) {
// 等待
try {
Log.d("testWaitNotify","主线程等待lock");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 0; j < 3; j++) {
Log.d("testWaitNotify","主循环第" + (j + 1) + "次");
}
// 主线程执行完毕,子线程标志位设为true
beShouldSub = true;
// 唤醒
Log.d("testWaitNotify","主线程唤醒lock");
lock.notify();
}
}
volatile进阶
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
该链接超级详细的讲解了volatile
Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)
在DEMO中我也详细的写了一个错误示范例子
/**
* 这是Volatile的一个错误示范
* 事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
* 可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性
* 自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存
* 那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
* 假如某个时刻变量inc的值为10
* 线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了
* 然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,增加1变成11,并把11写入工作内存,最后写入主存
* 然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
* 那么两个线程分别进行了一次自增操作后,inc只增加了1。
*/
private void testVolatileNo() {
final Test test = new Test();
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
test.increase();
}
if (finalI ==9) {
System.out.println("test.inc: " + test.inc);
}
}
}.start();
}
}
参考学习文章:
妈妈再也不用担心你不会使用线程池了(ThreadUtils) - 简书 (jianshu.com)
Android-多线程 - 简书 (jianshu.com)
深入理解线程和线程池(图文详解)weixin_40271838的博客-CSDN博客线程池
安卓Thread的运用 Thread.join()_ruiruiddd的博客-CSDN博客
Android进阶——多线程系列之wait、notify、sleep、join、yield、synchronized关键字、ReentrantLock锁_点击置顶文章查看博客目录(全站式导航)-CSDN博客