多线程的实现
进程: 启动一个可执行文件就是一个进程
线程: 线程是进程内部所做的完成的任务
-
继承Thread 重写Run方法
继承Thread启动线程
实例化对象, 调用start方法 -
实现Runnable接口 重写Run方法
实现Runnable接口启动线程
实例化对象, new Thread(对象).start
实现Runnable接口的类并没有start方法来启动线程, 只能通过new一个线程来启动, 或者采用匿名类new Thread(类名).start();
-
线程池实现多线程
-
匿名类实现多线程也很常用
-
lambda表达式实现
run并不是启动启动线程, 只有start才是启动线程
常用方法
-
当前线程暂停
Thread.sleep()
Thread.sleep() 会抛出InterruptedException 中断异常
单位是毫秒(一般为1000) -
加入当前线程
主线程会等当前线程结束才会继续运行(main即主线程).
线程1.join()
将线程1加入到主线程中, 只有等线程1结束后才会继续运行其他线程. -
设置线程优先级
线程1.setPriority();
参数有Thread.MAX_PRIORITY最大, Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。也可以自行设置数字等级。
设置优先级必须在线程启动前设置 -
临时暂停
使当前线程暂停, 让其他线程使用CPU资源
线程1.yield(); -
线程状态
得到该线程状态
线程1.getState();
线程的运行分为六个状态- new状态:新建线程对象
- Runnable状态: Runnable有两个状态组成, 一个是Ready状态, 就绪状态, 任务处于CPU的等待队列中; 一个是Running状态, CPU运行任务
- teminated状态: 线程结束或中断
- timewaiting状态: 时间结束自动唤醒
- waiting状态: 睡眠(等待被唤醒)
- blocked状态:被锁定状态
关于InterruptedException异常, 这是我在简书上找到的一篇文章简书
守护线程
守护线程和用户线程:
用户线程: 平常创建的普通线程
守护线程: 用来服务用户线程, 不需要上层逻辑介入
设置守护线程(线程启动前设置)
线程1.setDaemon(true);
当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。例:Java垃圾回收线程就是一个典型的守护线程;内存资源或者线程的管理,但是非守护线程也可以。
守护线程通常会被用来做日志,性能统计等工作。
注意事项:
(1) 当线程只剩下守护线程的时候,JVM就会退出;补充一点如果还有其他的任意一个用户线程还在,JVM就不会退出。
(2) 守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。
并发和并行
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的(实际情况并不存在)
线程之间的执行是无序的
多线程资源共享问题
多线程修改数据时会发生一种情况
一个增量线程, 一个减量线程
增加线程进入, 修改数据, 但还没来得及修改数据时。此时减量线程运行了。
此时的数据还没有被增量修改, 就已经被减量所调用了。增量线程先修改数据, 减量线程后修改数据。最终结果就会有差异。这就叫脏数据。
解决脏数据: 在当前线程访问数据期间, 其他线程不可以访问数据
- 当前线程获取到数据值,并进行运算
- 在运算期间,其他线程 试图来获取数据值,但是不被允许
- 当前线程运算结束,并成功修改数据值
- 其他线程,在当前线程结束后,才能访问数据值
- 其他线程 运算,并得到新的数据值
这时就要引入同步对象概念
同步对象
synchronized关键字
Object C1 =new Object();
synchronized (C1){
//代码块
}
synchronized表示当前线程独占对象C1。
在此期间, 如果有其他线程试图修改对象C1, 就会等待, 直到当前线程释放对C1的占用。
释放同步对象: synchronized块结束后自然释放, 或者异常抛出。
C1就是同步对象, 所有对象, 都可以被当做同步对象
synchronized的使用
同步对象在同一时间, 只能被一个线程占有(修改数据)。
Object C1 =new Object();
synchronized (C1){
C1.alter();
}
//每次都用这种方式来同步对象太麻烦
有两种更为方便的用法
- 把类直接做为同步对象, 同步对象可以是所有对象, 线程会访问对象进而修改数据。
//具体步骤
public class WOW extends Thread{
public int length = 0;
public void shout(){
synchronized(this){
length +=1;
}
}
public void run(){
this.shout();
}
}
public static void main(String[] args) {
final WOW w1 = new WOW();
int n = 1000;
for(int i =0;i<n;i++){
w1.run();
}
w1.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 在方法前使用synchronized修饰符
效果与第一种方法相同
在敲代码过程中, synchronized修饰的方法不如第一种方式好用, 个人使用观点
public class WOW extends Thread{
public int length = 0;
public synchronized void shout(){
length +=1;
}
public void run(){
this.shout();
}
}
//主方法同上
如果一个类的所有方法都被synchronized修饰, 那么该类就叫做线程安全的类。StringBuffer类就是线程安全的类。
同步对象在python中叫做互斥锁
死锁问题
线程1占有对象1, 等待对象2
线程2占有对象2, 等待对象1
此时就会出现死锁现象, 线程1等待对象2被释放, 线程2等待对象1被释放.
死锁是大忌!一定要合理使用synchronized
互斥锁
import java.util.concurrent.locks.*;
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
Lock是一个接口
Lock lock = new ReentrantLock();
lock()方法, 表示当前线程占用lock对象, 一旦锁定, 其他线程就不能占用了.
与synchronized不同的地方在于, synchronized块结束后会自动释放对同步对象的占用. 互斥锁必须调用unlock方法手动释放, 通常unlock方法会放在finally中.
trylock方法
在指定时间范围内试图占用, 成功占用就进行修改, 如果占用不成功, 就放弃.
因为有存在占用不成功的情况, 所有解除锁定就要进行判断.
locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
Thread.sleep(5000);
}
else{
}
线程间的交互
使用synchronized情况下
this.wait() 将占有this的线程等待, 并临时释放占有
this.notify() 使等待this的线程苏醒
两个方法都是放在线程会调用的方法中
this.wait()使用场景: 在某特定情况下, 将线程1占有的对象暂时释放, (线程1进入休眠状态)
对象被释放, 其他线程占有对象
其他线程操作结束后末尾加上this.notify(), 唤醒线程1.
public synchronized void Amethod(){
if(...){
this.wait(); //用try包裹
}
...
}
public synchronized void Bmethod(){
...
...
this.notify();
}
关于wait和notify方法
这两种方法并不是thread线程的方法, 是Object的方法.
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法.
wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
使用Lock情况下
Lock对象得到一个Condition对象, 然后调用Condition对象的await, signal, signalAll方法. 和synchronized类似
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();
总结
-
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现。
-
Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
-
synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。
线程池
线程池就是一个含有多个线程的容器, 将任务抛给线程池后, 线程池中的线程就会执行这个任务. 当有多个任务时就会将多个线程唤醒. 整个过程中都不需要创建新的线程, 而线程池中的线程也会循环使用.
使用线程池之前先自己编写一个线程池了解其原理, 再使用Java自带的线程池
线程池类ThreadPoolExecutor在包java.util.concurrent下
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
ThreadPoolExecutor有五个参数
ThreadPoolExecutor threadpool = new ThreadPoolExecutor(10,20,60,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
第一个参数表示线程池的初始线程个数, 10个线程
第二个参数表示如果初始线程不够用了, 额外增加的线程数. 当10个线程不够用时, 最多会自动增加10个线程, 即上限为20个.
第三个参数与第四个参数组合使用, 表示经过60秒后, 空闲的线程没有任务时就会被收回线程池.
第五个参数是存放任务的集合.
threadpool.execute(参数);
参数是要放入线程池的任务, 且必须是继承了Thread类或实现了Runnable接口.