Java多线程和线程同步
什么是线程安全
多个线程访问共同资源数据时,在某个线程进行写操作的同时其他线程同时读写操作了这个共享资源,从而导致了数据错误
进程和线程
- 操作系统中运行多个软件
- 一个运行中的软件可能包含多个进程
- 一个运行中的进程可能包含多个线程
CPU 线程和操作系统线程
- CPU 线程
- 多核 CPU 的每个核各自独立运行,因此每个核一个线程
- 「四核八线程」:CPU 硬件方在硬件级别对 CPU 进行了一核多线程的 支持(本质上依然是每个核一个线程)
- 操作系统线程:操作系统利用时间分片的方式,把 CPU 的运行拆分给多条 运行逻辑,即为操作系统的线程
- 单核 CPU 也可以运行多线程操作系统
线程是什么
按代码顺序执行下来,执行完毕就结束的一条线
UI 线程为什么不会结束?因为它在初始化完毕后会执行死循环,循环的内 容是刷新界面
常用的几种java线程
thread
/**
* 使用 Thread 类来定义工作
* (不推荐)
*/
static void thread() {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread started!");
}
};
thread.start();
}
thread.run 和 thread.start区别
调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行
- t.start()会导致run()方法被调用,run()方法中的内容称为线程体,它就是这个线程需要执行的工作。
- 用start()来启动线程,实现了真正意义上的启动线程,此时会出现异步执行的效果,即在线程的创建和启动中所述的随机性。
- start方法会调用JVM_StartThread方法去创建一个新的子线程,并通过run方法启动子线程
- start()方法会使得该线程开始执行;java虚拟机会去调用该线程的run()方法。
- 而如果使用run()来启动线程,就不是异步执行了,而是同步执行,不会达到使用线程的意义
- 调用run的时候会沿用主线程来执行方法,调用start方法的时候会用子线程执行方法。
- run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
runnable
/**
* 这种写法可以重用runnable
* 不推荐(线程管理性较低)
*/
private static void runnable() {
Runnable runnable = new Runnable() {
@Override public void run() {
System.out.println("Thread with Runnable started!");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
threadFactory
/**
* 工厂方式创建
*/
private static void threadFactory() {
ThreadFactory factory = new ThreadFactory() {
AtomicInteger count = new AtomicInteger(0); // int
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Thread-" + count.incrementAndGet()); // ++count
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started!");
}
};
Thread thread = factory.newThread(runnable);
thread.start();
Thread thread1 = factory.newThread(runnable);
thread1.start();
}
executor
/**
* 线程池
*/
static void executor() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
// 参数1 核心线程数 核心线程不会被回收,如100个线程 等执行完毕后 会保留5个
// 参数2 最大线程数 若超出最大值,则其他线程会排队
// 参数3 线程等待时间(也是等待被回收的时间)
// 参数4 时间单位
// 参数5
ExecutorService myExecutor = new ThreadPoolExecutor(5, 100,
5, TimeUnit.MINUTES, new SynchronousQueue<Runnable>());
myExecutor.execute(runnable);
//单个线程 用的很少
//Executors.newSingleThreadExecutor();
//固定线程数量的线程池
//Executors.newFixedThreadPool(20);
//延迟执行
//Executors.newScheduledThreadPool(20);
//Executors.newSingleThreadScheduledExecutor();
}
callable
/**
* 可理解为相当于一个有返回值的runnable
*/
static void callable() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Done!";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
while (true) {
//future.isDone()检查线程内的事是否做完了
if (future.isDone()) {
try {
String result = future.get();
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
break;
}
}
}
synchronized1Demo1:volatile关键字和AtomicBoolean
如下代码,我们尝试在主线程中修改running的值,1s之后让程序停止下来,但是结果并非如此,程序会一直执行
public class Synchronized1Demo implements TestDemo {
//private AtomicBoolean running = new AtomicBoolean(true);
private volatile Boolean running = true;
private void stop() {
running = false;
//running.set(false);
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
//while (running.get()) {
while (running) {
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
}
}
由于主程序和子程序同时操作了running,running在两个线程中互不干扰,所以程序不会马上停下来
volatile关键字
volatile:多个线程操作同一个变量,使用volatile关键字
如下用volatile修饰running,则程序1s后可以正常结束
private volatile Boolean running = true;
AtomicXXX
包装类,增加原子性和同步性
- AtomicInteger : 具有原子性和同步性的整形
- x++ : x.getAndIncrement()
- ++x : x.incrementAndGet() 所以 running可以用AtomicBoolean修饰
private AtomicBoolean running = new AtomicBoolean(true);
synchronized1Demo2:synchronized关键字
如下:我们希望最后输出x=2000000,但是最后结果总是不如人意
用volatile修饰x也不行,这是因为x++不是一个原子操作
public class Synchronized2Demo implements TestDemo {
private int x = 0;
//private volatile int x = 0;
private void count() {
x++;
//x++等价于如下两步操作,这不是一个原子操作,所以用volatile修饰x并不管用
//int temp = x+1;
//x = temp;
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
count();
}
System.out.println("final x from 1: " + x);
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
count();
}
System.out.println("final x from 2: " + x);
}
}.start();
}
}
输出
final x from 1: 504656
final x from 2: 1352457
synchronized关键字
synchronized同一时间不能被两个线程同时访问
- 同步性
- 互斥性
我们用synchronized修饰count方法,就可以得到期待的结果了
//synchronized关键字
private synchronized void count() {
x++;
}
synchronized1Demo3:Monitor监视器
如下代码,用synchronized修饰count和setName方法,已达到多线程下保护x、y、name值的目的,但是这样写不够好,因为我们保护了方法,而我们的实际目的是保护x、y、name的值
public class Synchronized3Demo implements TestDemo {
private int x = 0;
private int y = 0;
private String name;
private synchronized void count(int newValue) {
x = newValue; //monitor
y = newValue;
}
private synchronized void setName(String newName) {
name = newName;
}
@Override
public void runTest() {
}
}
synchronized会提供互斥访问,synchronized在修饰方法时会提供一个monitor(监视器),监视方法是否被其他线程调用,上面的代码中,两个方法会被同一个监视器监视/保护,所有方法在同一时间只能被一个线程访问,导致当一个方法被访问时,另一个线程无法访问任何一个方法,如图
Monitor
修改上面的代码,增加监视器,分别监视x、y和name,如图
代码如下
public class Synchronized3Demo implements TestDemo {
private int x = 0;
private int y = 0;
private String name;
private final Object monitor1 = new Object();
private final Object monitor2 = new Object();
private void count(int newValue) {
synchronized (monitor1) {
x = newValue; // monitor
y = newValue;
}
}
private void minus(int delta) {
synchronized (monitor1) {
x -= delta;
y -= delta;
}
}
private synchronized void setName(String newName) {
synchronized (monitor2) {
name = newName;
}
}
@Override
public void runTest() {
}
}
死锁
当出现多重锁的时候,锁互相持有,导致程序一直等待下去,就产生了死锁
单锁永远不会死锁
如下: 当线程1的count方法准备修改name的时候恰巧线程2的setName方法准备修改x,y,这个时候由于线程2持有了monitor2没有释放而导致线程1无法拿到monitor2继续操作name;(线程2同理)
private void count(int newValue) {
synchronized (monitor1) {
x = newValue; // monitor
y = newValue;
synchronized(monitor2){
name = ???;
}
}
}
...
private synchronized void setName(String newName) {
synchronized (monitor2) {
name = newName;
synchronized (monitor1) {
x = ???;
y = ???;
}
}
}
静态单例:双重检查锁
两次判空检查:
- 第一次检查:初始化检查
- 第二次检查:保证第一个线程创建之后后面等待的第二个线程不会重复创建
volatile修饰sInstance的原因:
SingleMan初始化可能是一个很复杂的过程,极端情况下:假设线程1的sInstance创建完毕,并在虚拟机中被标记为可用,但是其初始化过程并没结束,这就导致线程2在获取sInstance的时候拿到了一个并不完全正确的对象,通过volatile就能够保证sInstance在初始化之后可用
class SingleMan {
private static volatile SingleMan sInstance;
private SingleMan() {
}
static SingleMan newInstance() {
if (sInstance == null) {
synchronized (SingleMan.class) {
if (sInstance == null) {
sInstance = new SingleMan();
}
}
}
return sInstance;
}
}
悲观锁和乐观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
ReadWriteLockDemo
读写的锁
public class ReadWriteLockDemo implements TestDemo {
private int x = 0;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
private void count() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
private void print(int time) {
readLock.lock();
try {
System.out.print(x + " ");
} finally {
readLock.unlock();
}
}
@Override
public void runTest() {
}
}
线程间通信
Thread.stop和Thread.interrupt
- stop 暴力终止
- interrupted: 温和终止,内部标记为终止,不是立即终止,具体终止操作要在线程内部去做
如下代码:
- 用stop去终止线程时,程序马上终止,不会打印100万个数字
- 用interrupt终止线程时,程序会打印完毕100万个数字才终止,要想马上终止,需要在线程内部添加处理代码
public class ThreadInteractionDemo implements TestDemo {
@Override
public void runTest() {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
System.out.println("number: " + i);
}
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread.stop(); 已弃用
thread.interrupt();
}
}
添加interrupt中断处理代码
if (isInterrupted()) {
// 擦屁股工作
return;
}
完整代码如下
public class ThreadInteractionDemo implements TestDemo {
@Override
public void runTest() {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
if (isInterrupted()) {
// 擦屁股
return;
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
// 擦屁股 异常处理等
return;
}
System.out.println("number: " + i);
}
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread.stop(); 已弃用
thread.interrupt();
}
}
需要try-catch的原因:
线程在开始一秒后被中断,但是线程睡了10秒,这个时候就会抛出一个异常,线程1秒就可以马上中断了,而不要等待结束才中断
如果程序在睡眠中被调用了interrupt,那么下面的try-catch就可以捕获异常,及时return退出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.dsh.txlessons.threadinteraction.ThreadInteractionDemo$1.run(ThreadInteractionDemo.java:15)
Object.wait()和Object.notify()/notifyAll()
wait和notify/notifyAll需要配合使用
- 在未达到目标时 wait()
- 用 while 循环检查
- 设置完成后 notifyAll()
- wait() 和 notify() / notifyAll() 都需要放在同步代码块里
如下代码:
由于线程1先执行,所以打印结果是String: null,如果线程1等待2s线程2等待1s讲究可以正常打印String: thread
public class WaitDemo implements TestDemo {
private String sharedString;
private synchronized void initString() {
sharedString = "threadtest";
}
private synchronized void printString() {
System.out.println("String: " + sharedString);
}
@Override
public void runTest() {
final Thread thread1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
printString();
}
};
thread1.start();
Thread thread2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
initString();
}
};
thread2.start();
}
}
日常开发中,我们期待不管哪个线程先执行,都得到我们期望的结果,在本例中就是线程2后执行但是依然可以正常打印出初始化后的sharedString,这个时候我们就需要知道wait和notify/notifyAll了
wait
wait的作用:在未达到目标时 wait()释放锁
首先修改printString,当string为空时,用 while 循环检查,wait等待
private synchronized void printString() {
while (sharedString == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
System.out.println("String: " + sharedString);
}
但是这样修改之后程序会一直执行下去,不会打印,这个时候需要initString方法添加notifyAll
notify/notifyAll
wait之后,等待取值string的线程会排队,当initString初始化之后,通知等待的线程可以取值了,这个时候调用notifyAll就会通知其他所有等待的线程,相比于notify,notifyAll更加常用和安全
private synchronized void initString() {
sharedString = "threadtest";
notifyAll();
}
Thread.join()和Thread.yield()
join():让另一个线程插在自己前面
如下代码,thread1插在打印代码"xxx"前面,即thread1执行完毕之后才会打印"xxx",可以理解为打印代码在等待thread1执行;会先打印sharedString再打印xxx
如果没有join代码,那么会先打印xxx再打印sharedString
@Override
public void runTest() {
final Thread thread1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
printString();
}
};
thread1.start();
Thread thread2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
initString();
}
};
thread2.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("xxx");
}
}
yield():暂时让出自己的时间片给同优先级的线程
用在线程内部,让共同竞争的同优先级的其他线程先执行(可以理解为时间更短的wait,一下下)
Thread thread2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
yield();//暂时让出自己的时间片给同优先级的线程
initString();
}
};
thread2.start();
}
Android的多线程机制
Android 中HandlerThread机制的雏形
示例如下:
首先让线程3s后运行一次并退出,代码如下
class Main {
public static void main(String[] args) {
CustomThread thread = new CustomThread();
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.setTask(new Runnable() {
@Override public void run() {
System.out.println("hahahaha");
}
});
thread.quit();
}
}
CustomThread
public class CustomThread extends Thread {
private Runnable task;
private AtomicBoolean quit = new AtomicBoolean(false);
synchronized void setTask(Runnable task) {
this.task = task;
}
void quit(){
quit.set(true);
}
@Override
public void run() {
while (!quit.get()) {
synchronized (this) {
if (task != null) {
task.run();
task = null;
}
}
}
}
}
虽然如此,这样使用仍然并不方便,所以安卓引入了Looper
Looper
修改代码,创建内部类looper,将task和退出标记quit放在Looper内
public class CustomThreadImprove extends Thread {
Looper looper = new Looper();
public Looper getLooper() {
return looper;
}
@Override
public void run() {
looper.loop();
}
public void quit(){
looper.quit();
}
class Looper {
private Runnable task;
private AtomicBoolean quit = new AtomicBoolean(false);
synchronized void setTask(Runnable task) {
this.task = task;
}
void quit() {
System.out.println("looper.quit()");
quit.set(true);
}
void loop() {
System.out.println("looper.loop()");
while (!quit.get()) {
synchronized (this) {
if (task != null) {
task.run();
task = null;
}
}
}
}
}
}
Main
class Main {
public static void main(String[] args) {
CustomThreadImprove thread = new CustomThreadImprove();
thread.getLooper().setTask(new Runnable() {
@Override public void run() {
System.out.println("run baby");
}
});
thread.start();
try {
Thread.sleep(1000);
System.out.println("one seconds later");
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.quit();
}
}
输出结果
looper.loop()
run baby
one seconds later
looper.quit()
HandlerThread
本质:在某个指定的运行中的线程上执行代码
思路:在接受任务的线程上执行循环判断
HandlerThread的基本实现:
- Thread 里 while 循环检查
- 加上 Looper(优势在于自定义 Thread 的代码可以少写很多)
- 再加上 Handler(优势在于功能分拆,而且可以有多个 Handler)
Java 的 Handler 机制:
- HandlerThread:具体的线程
- Looper:负责循环、条件判断和任务执行,每次循环都去判断是否有新的任务
- 任务是通过Looper内部的MessageQueue(装着很多message)存放的
- Handler:负责任务的定制和线程间传递
ThreadLocal
每个线程的私有空间,不与其他线程共享(参考JVM资料)
如下:在每个线程中获取的threadNumber都是其线程内部保存的值
public class Main {
public static void main(String[] args) {
final ThreadLocal<Integer> threadNumber = new ThreadLocal<>();
new Thread() {
@Override
public void run() {
...;
threadNumber.set(1);
threadNumber.get(); //1
}
}.start();
new Thread() {
@Override
public void run() {
...;
threadNumber.set(2);
threadNumber.get(); // 2
}
}.start();
}
}
HandlerThread中ThreadLocal的运用
在HandlerThread的run方法中,有一系列对looper的操作
class HandlerThread {
...
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
...
}
比如在Looper的prepare方法中就创建了一个线程私有的Looper
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
Looper中的loop方法
Android主线程通过Looper.loop()检查消息
loop是一个死循环,Message msg = queue.next();拿到新的消息,通过msg.target.dispatchMessage(msg);处理新消息,msg.target是一个Handler对象,handler通过dispatchMessage方法处理了消息
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
在安卓开发中,Activity可以获取到Looper对象,looper可以用于创建handler
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Looper.myLooper();
Looper.getMainLooper();
HandlerThread handlerThread = new HandlerThread("Name");
//handler创建方式1
Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//自定义callBack处理消息
return false;
}
});
//handler创建方式2
Handler handler2 = new Handler(handlerThread.getLooper());
}
}
AsyncTask
AsyncTask的内存泄漏
耗时任务持有了外部Activity的引用,导致无法及时被GC
- 众所周知的原因:AsyncTask 持有外部 Activity 的引用
- 没提到的原因:执行中的线程不会被系统回收
- Java 回收策略:没有被 GC Root 直接或间接持有引用的对象,会被回收
- GC Root:
- 运行中的线程
- 静态对象
- 来自 native code 中的引用
- GC Root:
- 所以
- AsyncTask 的内存泄露,其他类型的线程方案(Thread、 Executor、HandlerThread)一样都有,所以不要忽略它们,或者认为 AsyncTask 比别的方案更危险。并没有。
- 就算是使用 AsyncTask,只要任务的时间不⻓(例如 10 秒之 内),那就完全没必要做防止内存泄露的处理。
Executor、AsyncTask、HandlerThead、IntentService 的选择
原则:哪个简单用哪个
- 能用 Executor 就用 Executor
- 需要用到「后台线程推送任务到 UI 线程」时,再考虑 AsyncTask 或者 Handler
- AsyncTask在Android R(11)中已经被弃用(@Deprecated)
- HandlerThread 的使用场景:原本它设计的使用场景是「在已经运行的指定线 程上执行代码」,但现实开发中,除了主线程之外,几乎没有这种需求,因为 HandlerThread 和 Executor 相比在实际应用中并没什么优势,反而用起来会麻 烦一点。不过,这二者喜欢用谁就用谁吧。
- IntentService:首先,它是一个 Service;另外,它在处理线程本身,没有比 Executor 有任何优势
关于 Executor 和 HandlerThread 的关闭
如果在界面组件里创建 Executor 或者 HandlerThread,记得要在关闭的时候(例如 Activity.onDestroy() )关闭 Executor 和 HandlerThread。
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdown();
handlerThread.quit(); // 这个其实就是停止 Looper 的循环
}