Java的多线程和线程同步
多线程主要设计两点:多线程如何正确使用,如何保证线程安全。
进程:操作系统上的一块独立的运行区域,有自己的数据管理,数据不共享。每隔进程内部是自己完整的程序逻辑,不能的程序之间本来就不应该共享资源;而同一个进程的多个不同线程,需要都能操作到这个进程的资源,程序才能正常运行。
线程: 运行在进程中,线程之间可以共享资源,所以线程安全的问题是重中之重。
操作系统线程和CPU线程是两回事,一般程序的线程是操作系统的线程。
Android APP,主线程不会结束,反复的刷新界面,AndroidUI线程为什么死循环不会卡界面的问题。UI线程是一个大循环,每一圈都是一次界面刷新操作,而不是对某一次界面刷新过程进行内部的死循环,所以不会卡死界面。
线程创建的几种方式
重写run方法 (不推荐)
Thread thread = new Thread() {
@Override
public void run() {//重写run方法
System.out.println("Thread start!");
}
};
thread.start();
runnable (不推荐)
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
//赋值给target,在run中执行,runnable可以进行重用
Thread thread = new Thread(runnable);
thread.start();
ThreadFactory 工厂方法创建线程,统一线程的初始化
ThreadFactory factory = new ThreadFactory() {
//AtomicInteger
AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
//统一线程的初始化操作
return new Thread(r, "Thread-" + count.incrementAndGet());
}
};
//runnable的复用
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();
Executes 线程池,开发中常用的方式
一个线程池的线程数定义为动态值:CPU核心数的1倍,CPU核心越多线程池越大,这样可以在高性能的手机上有更大的线程池,相当于按手机的能力来分配线程池的大小。
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
//newCachedThreadPool 线程无限大,闲置60S回收
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
- 自带的几种线程池的创建方式
创建线程池的参数说明:ThreadPoolExecutor(5,20,60,S) 默认线程数5个,最大线程数是20个,当线程执行完毕,60S后没有被使用则进行回收。 shutdown: 不会终止当前排队的任务,但是会阻止后面的任务进来,当前的任务队列执行完毕之前,后面都不会有任务排队进来。 shutdownNew: 结束所有的线程,调用线程的interrupt方法。
Executors.newCachedThreadPool(); //线程无限大,闲置60s回收
Executors.newSingleThreadExecutor();//只有1个线程,线程运行完毕,立即回收
Executors.newFixedThreadPool(10);//固定线程数,使用完立即回收,用于集中处理多个瞬时爆发的任务
//例如如下代码:处理20个bitmap
// List<Bitmap> bitma = ...
// ExecutorService executor1 = Executors.newFixedThreadPool(20);
// for (Bitmap bitmap: bimaps) {
// executor1.execute(processImagesRunnable);
// }
// //只允许现在的任务队列执行,不允许在添加排队任务
// executor1.shutdown();
// processImagesRunnable // processImage
Executors.newScheduledThreadPool(10);//定时的线程池
自定义线程池:
//自定义线程池
ExecutorService myExecutor = new ThreadPoolExecutor(5, 100,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
- callable 可以有返回值
Future.get()
会阻塞主线程,等待后台任务执行完毕,对于submit
不会阻塞主线程。
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1500);
return "Done";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
try {
System.out.println(future.get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
诶,我们使用线程的目的就是不阻塞主线程,但是这里阻塞了主线程,Java API层只能做到这一步,我们可以进行检测future.isDone
子线程是否执行完毕了
while (true) {
System.out.println("其他指令,不会卡主线程");
if (future.isDone()) {
try {
System.out.println("start");
System.out.println(future.get());
System.out.println("end");
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
线程安全
线程安全的本质:某些资源被多个线程同时访问,导致资源在一个线程对它写到一半的途中被其他线程写或读,或在读到一半的途中被其他线程写,导致出现数据错误。
volatile
volatile 关键字可以保证数据的可见性,当一个线程进行写操作时,写完会立即同步数据;当一个线程读取数据时,会先进行同步数据再读取
如下代码:通过isRunning
来控制子线程的结束,在主线程修改isRunning
,然后来看子线程中的while
循环是否结束?
public class Synchronized1Demo implements TestDemo {
private boolean isRunning = true;
private void stop() {
isRunning = false;
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
while (isRunning) {//=false 子线程就会结束
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
}
}
当执行了stop()
方法,修改了isRunning = false
,但是子线程并没有结束,还在执行,Why??
这就是典型的线程安全的问题,首先来看Java中的线程模型:主内存中是存储了资源数据,而每个线程有自己的工作内存,当要用到主内存中的资源,需要将资源拷贝到自己的工作内存中,当资源发生改变就需要同步到主内存中去。
上述的代码中isRunning,主线程中的工作内存对isRunning进行了修改,但是并没有同步到主内存中,导致子线程工作内存isRunning一直是true. 通过volatile关键字可以将isRunning的变化及时的同步到主内存中,同时子线程读取数据也会从主内存中读取。voltaile保证了变量的可见性,当一个变量在一个线程发生变化,会同步到主内存,另一个线程读取时会立即进行同步,首先从主内存中读取数据。
public class Synchronized1Demo implements TestDemo {
private volatile boolean isRunning = true;
private void stop() {
isRunning = false;
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
while (isRunning) {//=false 子线程就会结束
}
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
}
}
synchronized
synchronized 保证了原子性,一组操作是不可分割的原子,不能被打断,同时也保证了可见性,在加锁前会清空工作内存变量值,重新从主内存中读取,在释放锁之前会将变量的值刷新回主内存中。
如下代码,来了解synchronized
的本质:根据上面所学习的volatile
关键字,保证变量值的在多线程下的同步性,那么下面代码看似没有问题,x的值最终应该为:2_000_000
private volatile int x = 0;
private void count() {
x++;
}
@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();
}
但实际结果:这是为什么呢?volatile 不是保证了x的值具有同步性吗?
其实x++
是分两步进行编译:int temp = x+ 1
x = temp
不是一个原子操作 而volatile只是保证了x具有同步性,但是temp不具备同步性了,如下图模拟执行的流程:
线程之间是共享主内存中的数据的,假如线程1,一直执行执行到temp = 10了这时候发生线程切换,注意x = temp没有执行,虽然x具有了同步性,但是x=temp没有执行,那么x并且没有同步到主内存中,x 的还是9,那么这时候线程2开始执行 temp = 9+1 = 10 x= 10,一直执行到x=15,这是切换到线程1,线程1继续上一次的操作,然而temp是在线程1工作内存中存储了值为10,那么下一步为x赋值 x=10,由于volatile的同步性,将x同步到了主内存中,这是在切换线程2拿到的x就是10了,这也是为什么最终打印的结果小于预期的值。
这时候就需要有synchronized,而synchronized就是为了解决volatile不能处理的问题,volatile只能保证变量的值具有同步性,不能保证一组操作具有同步性,synchronized就实现了一组操作具有原子性,不可分割的。synchronized 会保证在每次执行时都会从主内存同步数据,执行完毕后将数据刷新回主内存中。
//同步性问题 多个线程各自的内存拷贝
//volatile 在这里不会起作用,X++是两步操作 int temp = x + 1 x = temp 导致不是一个原子操作(不可拆的操作)
private int x = 0;
//synchronized 保证count方法具有原子性,一个线程必须执行完count方法,下一个线程才能执行count
//内部的变量 都会具有同步性 synchronized 保证了 同步性和原子性
private synchronized void count() {
x++;
}
@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();
}
这时候在执行代码就能拿到想要的结果了:
如下图来看一下synchronized的执行流程:
- 首先被synchronized包裹的一组操作只允许一个线程被访问,会上锁,其他线程要访问需要进行等待上一个线程执行完毕才可以访问
- 线程1 获取到synchronized锁,会先从主内存中同步数据,然后执行操作,当线程执行结束,会将数据刷新到主内存中,最后释放锁
- 然后其他的线程会公平竞争锁,拿到锁的线程执行,如此反复
synchronized 的使用是不是所有的操作都在方法上加上synchronized关键字呢?这要做会有什么问题吗?
如下代码:看起来没有任何问题,一个线程访问count2(),一个线程访问setName2(),预期的执行结果就是:线程1输出,线程2输出,但是实际结果线程1执行完count2才会执行setName2方法,而不是线程1和线程2各执行自己的方法???这是为什么呢?完全是两个线程也没有共享一样的资源
private int x = 0;
private int y = 0;
private String name;
private synchronized void count2(int newValue) {
x = newValue;
y = newValue;
System.out.println("newValue = " + newValue);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void setName2(String name){
System.out.println("name = " + name);
}
@Override
public void runTest() {
new Thread() {
@Override
public void run() {
count2(10);
}
}.start();
new Thread() {
@Override
public void run() {
setName2("jakeprim");
}
}.start();
}
这其实是synchronized的一个特性:synchronized 具有互斥访问的特性,会为方法提供一个监视器monitor,上述两个方法count2和setName2使用的是同一个监视器,所以当访问count2的时候,就不能访问setName2了,这个monitor的作用在于监视访问的线程,当一个线程访问了,这时候monitor就是设置一个标记,其他线程就不允许访问了。
这种设定的意义:在于开发者可以指定设置的monitor,哪些操作共享一个monitor,哪些操作是其他的monitor进行监视, 这些操作是互不影响的
如下代码,synchronized(this) == synchronized void count2()
和作用在方法上是等价的
synchronized (this) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
x = newvalue;
y = newvalue;
}
为setName2指定一个monitor,不使用默认的monitor
private static final Object object2 = new Object();
private final Object monitor1 = new Object();
private void setName(String newName) {
synchronized (monitor1) {//指定monitor
name = newName;
}
例如如下代码,使用一个monitor的情况:当一个线程访问count方法,那么肯定不希望其他的线程访问minus去改变x和y的值。
/**
* 当一个线程访问count,那么是不希望另一个线程访问minus的,这是为什么monitor要管理多个方法
*
* @param delta
*/
private synchronized void minus(int delta) {
x -= delta;
y -= delta;
}
private void count(int newvalue) {
synchronized (this) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
x = newvalue;
y = newvalue;
}
}
这时候的monitor的分布情况如下图:精准的控制monitor
synchronized 经常会被问到死锁的问题,其实只有多重锁才会造成死锁的问题,如下代码死锁的情况:
monitor1 和 monitor2 两个监视器,假如线程1执行了count方法,持有了monitor2的锁,这时候线程2访问了setName方法,持有了monitor1的锁,线程1想要执行完毕,就需要拿到monitor1的锁执行,线程2要想执行完毕就要拿到monitor2的锁执行,两个线程都无法释放锁造成死锁的问题
private void count(int newvalue) {
synchronized (monitor2) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
x = newvalue;
y = newvalue;
//死锁的问题 当setName 被锁定,这里就不能执行 只有多重锁才会出现死锁
synchronized (monitor1){
name = "xxx";
}
}
}
private void setName(String newName) {
synchronized (monitor1) {//指定monitor
name = newName;
//死锁的问题
synchronized (monitor2){
x = 1;
y = 2;
}
}
}
synchronized 整体的执行流程如下图所示:
常用的单例模式:
public class SingleMan {
private static volatile SingleMan sInstance;
private SingleMan() {
}
static SingleMan getInstance() {
if (sInstance == null) {
synchronized (SingleMan.class) { //等价于 static synchronized
if (sInstance == null) {
sInstance = new SingleMan();//volatile 保证构造方法 初始化完毕
}
}
}
return sInstance;
}
}
Lock
public class ReadWriteLockDemo implements TestDemo {
private int x = 0;
ReentrantLock lock = new ReentrantLock();
//将锁分成 读锁和写锁 更精细的控制 进行写操作的时候,不能进行读。在没有写操作的时候,就可以读
//这样就不会因为写操作的冲突,或者写了一半的时候进行了读操作,或者写了一半的时候进行写操作等等这些问题
//线程安全:主要是共享资源的安全问题
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
Lock readLock = reentrantReadWriteLock.readLock();
//写锁
Lock writeLock = reentrantReadWriteLock.writeLock();
private void count() {
// lock.lock();//上锁
writeLock.lock();
try {
x++;
//... 如果中间抛异常 就无法解锁了
} finally {
// lock.unlock();//解锁
writeLock.unlock();
}
}
@Override
public void runTest() {
readLock.lock();
try {
System.out.println(x);
} finally {
readLock.unlock();
}
}
}
正确停止线程
Thread.stop() 是非常危险的,因为它会直接杀死线程,有几率让线程在某些工作运行到一半的时候被结束,从而系统被强制停留在某种中间状态,进而导致问题。 Thread.interrupt() 来结束线程是安全的,因为它并不会直接杀死线程,而是告诉线程【外界希望你停止】,具体的结束工作由线程自己来完成,所以更加安全。
public class ThreadInteractionDemo implements TestDemo {
@Override
public void runTest() {
Thread thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(10000);
//等待状态的线程终止
} catch (InterruptedException e) {
//不会改中断的标记的 还是false
//擦屁股
return;
//在等待的过程中 中断了线程 会直接抛出一个异常 直接打断就可以 在等待过程中不会改资源的
}
// for (int i = 0; i < 1_000_000; i++) {
// //Thread.interrupted()// 会把标记重置为false 会改标记的状态的
// isInterrupted 不会改状态
// if (isInterrupted()) {//如果中断状态被标记为true了 则结束线程 在哪里中断 需要根据业务来判断
// return;
// }
// System.out.println("number:" + i);
// }
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread.stop();//停止线程 但是stop被弃用了,会导致线程不可预期的错误,stop强制中断线程
// 假如线程正在修改两个变量,这是一个变量修改了 另一个变量被中断了a=100 b=50 导致中间状态 程序的状态是不可控的
// 使用外力终止线程是非常危险的,本身是一个不靠谱的机制
//interrupt 是打断,需要自己去程序中断线程,而Thread不会打断线程,只是把线程标记为中断状态,需要自己去中断
//不是强制的,这样就能避免了stop导致的不可预期的错误
thread.interrupt();
}
}
线程间通信
Object.wait() 经常需要外面包着while循环,因为线程在等待过程中被叫醒的原因是不确定的,所以醒来后需要重新判断条件是否达成。
public class WaitDemo implements TestDemo {
private String sharedString;
private Object monitor = new Object();
private void initString() {
synchronized (monitor) {
sharedString = "jakeprim";
monitor.notify();//通知 等待区的出来一个,重新进行公平竞争锁
// notifyAll();//通知 等待区的全部拿出来
}
}
private void printString() {
// if (sharedString != null) {
// System.out.println("String:" + sharedString);
// }
//锁住了 initString 拿不到锁 永远在等待了
// while (sharedString == null) {
// }
synchronized (monitor) {
while (sharedString == null) {
try {
//wait 线程进入等待区 排队拿锁,会释放锁
monitor.wait();
} catch (InterruptedException e) {
}
}
System.out.println("String:" + sharedString);
}
}
@Override
public void runTest() {
final Thread thread2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//初始化之后 通知另一个线程
initString();
}
};
thread2.start();
final Thread thread1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//等待2号线程执行完毕
// try {
// thread2.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// yield(); 暂时让出 和自己同优先级的线程
printString();
}
};
thread1.start();
//join 运行在主线程前面,后面的不在执行了
try {
//让线程变成串行的关系
thread1.join();//等待状态 interrupt 中断
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("haha");
}
}
Android 的多线程机制
Handler 只能往HandlerThread插任务,而HandlerThread其实是无线循环,因此可以在每一环都做一次任务的检查与执行 AsyncTask 内部持有线程,而运行中的线程属于GC Root 可能会导致内存泄漏
public class CustomThread extends Thread {
Looper looper = new Looper();
@Override
public void run() {//注意不要在run 设置synchronized 否则锁处于一直锁定的状态
//永不结束的线程
looper.loop();
}
class Looper {
//要执行的任务
private Runnable task;
//线程安全的AtomicBoolean 保证同步性
private final AtomicBoolean quit = new AtomicBoolean(false);
synchronized void setTask(Runnable task) {
this.task = task;
}
void quit() {
quit.set(true);
}
void loop() {
while (!quit.get()) {
synchronized (this) {
if (task != null) {
//执行任务
task.run();
//任务执行完毕清空
task = null;
}
}
}
}
}
}
Service 后台线程的持续的空间 和 IntentService 后台任务