前言
java程序天生是多线程的,多线程是并发编程必须掌握的基础概念
线程 进程的基本概念
进程是操作系统分配资源的最小单位 包括分配CPU IO等
线程是CPU调度的最小单位
并行(是同一时刻可通行数)、并发(是单位时间内 通行数)
并行:同一时刻运行线程数,比如四车道的马路的并行数就是4。
并发:一时间段的线程数(一般以秒为单位),比如四车道的马路一秒通过的车辆数是100,并发就是100。
并发编程好处
多线程下载快
线程安全问题
因为一个进程下所有线程都是对这个进程的资源进行操作的,这就导致存在线程安全问题,也就可能产生死锁 线程多了也是耗性能的 因为cpu切换也是需要时间周期的 ,所以线程数量是有限制的 这就引入线程池概念。
线程分类
单线程、多线程(多线程又存在线程池的概念)。
1、单线程
比如我们的UI线程 就是一个单线程。
单线程的开启方式
继承Thread、实现runnable接口、实现callable接口 直接上代码!!!
//创建一个单线程
public class SingleThreadDemo {
private Thread thread2;
private SingleThread thread1;
private Thread thread3;
//创建单线程的几种方式
public void createThread() {
//方式1 extend Thread
thread1 = new SingleThread();
//开启线程 让线程处于就绪状态 等待cpu调度
thread1.start();
//方式二 实现runnable接口
thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2名字"+thread2.getName()+"id:"+thread2.getId());
}
});
thread2.start();
//方式3 :实现callable接口 需要用FutureTask将Callable包装
//FutureTask 是继承Runnable接口的 Callable不是
//这种方式的区别是执行call后是有返回值的
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("线程3名字"+thread3.getName()+"id:"+thread3.getId());
return "你好";
}
});
thread3 = new Thread(futureTask);
thread3.start();
try {
System.out.println("线程3返回值" + futureTask.get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
class SingleThread extends Thread {
//重写run方法
@Override
public void run() {
//实现自己的操作
System.out.println("线程1名字"+thread1.getName()+"id:"+thread1.getId());
}
}
}
注意:
1、thread.start方法并不会让线程直接开启,而是单纯的将线程改为就绪状态,真正是否执行还需要等cpu调度。
2、同一个线程不能多次调用start方法,这样会报错的
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
// Android-changed: throw if 'started' is true
if (threadStatus != 0 || started)
throw new IllegalThreadStateException();
单线程如何结束呢?有哪几种方式
当线程执行完run方法后就会自动结束,如果想人为去结束 那就只有interrupt方法(而且这个方法是协作式的,意思就是说是告诉cpu一个中断信号,而不是强制结束,具体能不能结束它决定不了,比较卑微)。
以下方法不建议用 (不释放锁是非常危险的一个信号)
1、thread.supspend():线程挂起,但是不会释放锁资源 所以会出现死锁。
2、thread.stop():会立马杀死资源,不管线程有没有释放掉,会导致内存泄漏,也会导致资源丢失。
3、thread.onreusme(): 将挂起线程重新开启
4、thread.ondestory():
那单线程中哪些方法建议使用呢? (会释放锁)
1、thread.wait():让线程进入等待状态,,只有等待另外线程的通知或被中断才会返回,会释放锁
2、Thread.sleep(xxx):也是让线程处于等待状态,但是不会释放锁,这点需要注意
3、notify():唤醒线程,但是不能保证具体唤醒哪个线程,所有如果是多个线程的话,尽量采取notifyAll(),因此该方法使用比较少。
4.notifyAll():唤醒所有线程。比较多用
5、thread.interrupt():发出中断线程的信号,中断不中断取决于线程本身 释放锁
6、thread.interrupted()(是static方法):判断中断状态,且将中断状态给重置(从中断状态重置为非中断状态) 判断不怎么用这个方法
7、thread.isInterrupted():判断中断状态,且不将中断状态给重置 判断多用这个方法
8、thread.join():把指定线程加入到当前线程里面去(任务的插队),这种会让
9、thread.yield():当前的cp线程让出cpu的执行权,但是可以重新选中,又开始执行,不释放锁 用的非常少。
上述方法注意事项
注意1: wait是Object类中的方法,而sleep是Thread类中的静态方法。
注意2: 调用wait方法的线程,不会自己唤醒,需要线程调用,通过notify / notifyAll(这俩个方法不是线程独有的是Oject拥有的)
注意事项3: sleep方法会自动唤醒,如果时间不到,想要唤醒,可以使用interrupt方法强行打断。
注意事项4: 在调用wait()之前,线程必须要获得该对象的对象级别锁
单线程常用方法调用和线程状态图 (非常非常重要!!!)
小结 单线程的内容就讲到这里了
2、多线程
我们开发任何项目都不可能只存在于单线程里面操作,所以多线程是必备掌握的知识。只是我们使用多线程中需要注意的地方还是非常多的,比如怎么样让多个线程对同一数据操作,还得保证安全呢等等之类的问题
线程安全问题
涉及到线程安全问题,我们就需要通过加锁的形式去保证数据安全。那锁分为哪几种呢?
隐式锁(Synchroinzed)、显示锁(lock)
synchronized
当多个线程对同一变量进行累加的时候 如果放任不管 会导致最终的结果数据不是我们理想的数据 如当一个全局数据count值为0 的时候 count= 0, 开启2个线程对数据进行10000次加值操作 发现最后很多时候值不为20000 所以我们需要加锁去保证。
package com.xm.studyproject.java.thread;
public class SyncDemo {
int count = 0;
private static Object object = new Object();
//不加锁
public void countAdd() {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}).start();
try {
Thread.sleep(1000);
System.out.println("值111:" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//加锁
public void countAddSyn() {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
}).start();
try {
Thread.sleep(1000);
System.out.println("值222:" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized虽然可以实现多线程同步(synchronized 可以作用在方法、对象、类上) 但是如果多个线程对自己的变量进行操作 怎么保证呢???
ThreadLocal的使用
package com.xm.studyproject.java.thread;
import androidx.annotation.Nullable;
public class ThreadLocalDemo {
private int count = 0;
private ThreadLocal<Integer> threadLocal = new ThreadLocal() {
//重写这个方法
@Nullable
@Override
protected Integer initialValue() {
return count;
}
};
public void test() {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName()+"xxx :"
+threadLocal.get());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName()+"xxx :"
+threadLocal.get());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName()+"xxx :"
+threadLocal.get());
}
}
}).start();
}
}
synchronized关键字去加锁的问题在哪?
问题1: 如果一个操作通过synchronized获取到锁后不释放 如果其他操作也去获取锁 会一直拿不到直到前一个操作释放锁 (如果当前锁 是锁的当前对象 那么其他操作这个对象的锁也是获取不到 如果是类 是一样的)
//加锁
public void countAddSyn() {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println("sleep1:" + count);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println("sleep2:" + count);
}
}
}).start();
try {
Thread.sleep(0);
System.out.println("值222:" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
问题2: 因为一旦锁了每次就会导致其他操作处于等待操作,而且当是读数据的时候 需要等到第一个读完才能操作 这个是非常影响性能的。
怎么解决synchronized不能释放锁的问题? 采用lock
显示锁lock
三个比较重要的方法:Lock.lock(),Lock.unlock(),Lock.tryLock();
package com.xm.studyproject.java.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
//可重入锁 synchronized也是可重入锁
//当Synchronized递归重复去调用的时候 如果是不可重入锁会导致拿不到锁 把自己锁死
// 导致后续代码执行不到 也不释放锁 所以Synchronized实现可重入锁
private Lock lock = new ReentrantLock();
private int count = 0;
public void test() {
for (int i = 0; i < 3; i++) {
new Thread(new MyRunnable()).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
//加锁
lock.lock();
//尝试获取锁 参数可以设置获取锁的时候
//lock.tryLock();
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println("LockDemo:"+count);
//可释放锁 但是这个释放最好放在finally里面执行 不然担心try的时候执行不到这行代码
lock.unlock();
}
}
}
读写锁
上面方法虽然解决了 锁不释放的问题,但是如果多个操作去读数据 还是必须等到前一个操作读完才能执行 这还是影响操作的,基于这个我们发现还有读写锁这个概念,读写锁(就是针对读的时候 各个操作之间是可以同时进行的 但是写的时候依然只能按操作有序去写,同时读的时候是禁止写的操作,写的操作是禁止读的操作),在我们实际项目中 读的场景是远远多于写的场景
例子
//读写锁 ReadWriteLock是接口
private ReadWriteLock locks = new ReentrantReadWriteLock();
//读锁
private Lock readLock = locks.readLock();
private int count = 0;
private long timeMillis;
-------------------------
public void test2() {
timeMillis = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
new Thread(new MyRunnable2()).start();
}
}
class MyRunnable2 implements Runnable {
@Override
public void run() {
//加锁 读锁
readLock.lock();
//lock.tryLock();
for (int i = 0; i < 100000; i++) {
count++;
}
System.out.println("时间长度:"+(System.currentTimeMillis()-timeMillis));
readLock.unlock();
}
}
从上面2个例子 你直观会发现读写锁比单纯锁速度会快很多
补充 contadtion的使用
相关方法
Condition condition = lock.newCondtion();
condition.signal()(类似于notify())
condition.signalAll()(类似于notifyAll())
condition.await()(类似于wait())
这里注意下 notify()唤醒线程是没法指定的,但是signal()是可以指定的。
公平锁 就是优先顺序去拿到锁 非公平则不是 常用都是非公平锁 不然cpu很多时候会处于等待状态
线程池(ThreadPoolExector)的使用
一个应用中线程的数量是有限制的,其次线程频繁的创建销毁也是需要时间、消耗性能的。那有没有什么办法做到线程的复用呢,这里我们就引入线程池的概念。
package com.xm.studyproject.java.thread.singlethread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
/**
* 线程池实例
*/
private ThreadPoolExecutor executor;
/**
* 核心线程数
*/
private int CORE_NUM = 5;
/**
* 最大线程数
*/
private int MAX_NUM = 10;
/**
* 阻塞队列 这边采用的是数组阻塞队列 阻塞队列有8种实现方式
*/
private BlockingQueue blockingQueue = new ArrayBlockingQueue(100,false);
/**
* 线程空闲最大时间(空闲时长超过这个值线程会被回收)
*/
private int KEEP_ALIVE_TIME = 3000;
/**
* 线程工厂 主要是命名 使用的不多 而且我这样使用是错误的 看源码发现 这里面是需要创建线程的
*/
// private ThreadFactory threadFactory = new ThreadFactory() {
// //这个对象是需要创建一个线程的
// @Override
// public Thread newThread(Runnable r) {
// return Thread.currentThread();
// }
// };
/**
* 自定义拒绝策略 系统提供了四种策略
*/
private RejectedExecutionHandler executionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
}
};
//获取线程池
public void getThreadPool() {
if (executor == null) {
executor = new ThreadPoolExecutor(CORE_NUM, MAX_NUM, KEEP_ALIVE_TIME,
TimeUnit.MILLISECONDS, blockingQueue);
}
}
// 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
private void execute(Runnable runnable) {
executor.execute(runnable);
}
//任务数
private int taskCount = 1000;
public void test() {
for (int i = 0; i < taskCount; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+":线程打印处理");
}
};
//为啥会需要做这么个操作呢 就是当taskCount非常大的时候比如10万 而且这10万个任务可以说是一瞬间创建完成的
//那么我的阻塞队列+我的线程是没法一瞬间处理完的 那就会崩溃
// try {
// Thread.sleep(50);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (executor != null) {
execute(runnable);
}
}
}
}
上面注意的就是基于任务数量的耗时去创建对应线程数。
向线程提交任务的方式
提交任务2种方式 一种execute 一种是submit(这个是有返回值的)
我们创建线程的种类
1、固定线程池
创建一个线程池,该线程池重用固定数量的线程来执行任意数量的任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
2、缓存的线程池
该线程池根据需要创建新线程,但在可用时将重用以前构造的线程。如果任务长时间运行,则不要使用此线程池
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
3、调度线程池
创建一个线程池,该线程池可以调度命令在给定延迟后运行或定期执行。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newScheduledThreadPool(10);
4、单线程池
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newSingleThreadExecutor();
5、工作窃取线程池
创建一个线程池,该线程池维护足够的线程来支持给定的并行级别。这里的并行级别是指在多处理器机器上,在单点时间内执行给定任务所使用的线程的最大数量。
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newWorkStealingPool(4);
一个任务过来了 是如何添加到线程池的流程 (核心概念!!!!)
添加任务流程 如果任务小于coreSize 那就新建线程处理 直到coreSize处理不完 那就先提交到BlockQueue中 如果BlockQueue满了 那就看MaxSize 大小 继续新建线程 如果maxSize不够 那就进入拒绝模式
拓展1 (8种阻塞队列源码分析)
参考:8种阻塞队列源码分析
拓展2
子线程可以更新Ui 但是不安全 如界面错乱 只是不安全而已
ThreadLocal实现静态变量隔离
后续
多线程是一门比较浩大的学问,针对这个知识还需要了解为啥不能线程池 设计的阻塞队列形式不一样等等问题 后续能力提升再补充 已经多线程的源码分析