我们经常听到一些大佬说一些概念,比如线程不安全,那到底什么是线程不安全呢?
线程不安全指的是,我们在多线程的环境下,操作一些共享数据的时候可能会让我们无法得到期望的结果
复制代码
线程
为什么会出现线程呢?
线程就好比一个人,俗话说的好,众人拾柴火焰高,而多线程也是这个道理!
复制代码
我们先来看一下线程6种状态的状态
线程状态
-
New(新生态)
-
Runnable(可运行态)
在可运行态中又可以分为,Running(运行态)和Ready(就绪态)
-
Running(运行态)
运行态指的是,该线程已经获取了CPU的时间片,简单来说,就是该线程正在运行
-
Ready(就绪态)
而就绪态指的是,该线程万事俱备只欠“东风”,就差CPU给他分配时间片了
-
-
Blocking(阻塞态)
线程在获取锁失败之后,就会进入该状态,当该线程获取了锁就会结束该状态,所有阻塞状态的线程都会放在阻塞队列中。
- Waiting(等待态)
当处于运行态的线程调用wait(),park(),join()方法后,就会进入该状态,处于等待态的线程,会释放CPU时间片,并且会释放资源(例如锁),这个状态下的线程只能等待其他线程来唤醒它。
- Timed Waiting(超时等待态)
超时等待态和等待态类似,不过这个状态的线程不需要显式地去唤醒,这个状态的线程在超过一定时间后,将由系统自动唤醒
- Terminated(结束态)
线程结束后的状态
线程的三种使用方式
线程的使用方式有三种,分别是实现Runnable接口、实现Callable接口、继承Thread类
-
实现Runnable接口
创建一个自定义类然后实现该接口,实现该接口的run()方法 然后在我们需要使用该类的地方直接去实例化即可 复制代码
-
实现Callable接口
创建一个自定义类然后实现该接口,实现该接口的call()方法 然后通过开启线程池服务来创建该类 复制代码
-
继承Thread类
创建一个自定义类然后继承该类,需要重写该类的run()方法 这种方式其实和实现Runnable接口的方式类似,因为Thread类也实现了Runnable接口 我们在实现的时候直接去实现即可 复制代码
三种方式的对比
其实我们更加推荐实现Runable接口的这种方式,因为相较于其他两种,实现Callable接口这种方式使用起来更加繁琐,而继承Thread类的这种方式是继承,我们都知道在Java中是不支持多重继承的,但是支持多重实现,并且用起来实现Runnable接口这种方式也更加简单
线程之间的协作工作
join()方法
我们在很多情况下,会在一个线程中调用另一个线程的方法,这个时候我们就会使用到join方法了
在这里我们先定义一个线程类,为了方便我们直接去继承Thread类
public class YlOneThread extends Thread {
@Override
public void run() {
System.out.println("我是亚雷1线程");
}
}
复制代码
在这里我们又定义了一个线程类,不过在这个线程类中需要使用前面那个线程
public class YlTwoThread extends Thread{
private YlOneThread thread;
public YlTwoThread(YlOneThread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是亚雷2线程");
}
}
复制代码
最后我们再创建一个测试类来测试
public class test {
public static void main(String[] args) {
YlOneThread Thread1 = new YlOneThread();
YlTwoThread Thread2 = new YlTwoThread(Thread1);
Thread2.start();
Thread1.start();
}
}
复制代码
我们可以很明显的看到再线程2中调用了线程1不出意外我们得到了这样的结果
我是亚雷1线程
我是亚雷2线程
复制代码
在这里虽然是线程2先启动,不过在线程2中调用了线程1,线程2会先等待线程1完成然后继续执行
wait()、notify()、notifyAll()方法.
在这里我就简略的说一下这些方法、因为这些方法并不是来自于JUC包下的而是来自于Object类下面的
-
wait()方法
这个方法在前面我们也见过了,让线程进入等待态并且也会释放资源(锁) 复制代码
-
notify()、notifyAll()方法
这两个方法用于唤醒线程、不同的是一个是唤醒所有线程而另一个不是 复制代码
wait()和sleep()
我们经常拿这两个方法进行比较、因为它们在我们浅层的认识中都是使线程进行等待
其实它们大相径庭
wait()方法会释放资源(锁),这在前面我们已经知道了, 而sleep()方法并不会释放资源(锁)
wait()方法是基于Object的方法,而sleep()是基于Thread类的方法
await()、signal()、signalAll()方法
而在JUC中我们使用这些方法来实现线程间的调度
public class AWaitTest {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void methodOne(){
lock.lock();
try {
System.out.println("方法一执行......");
condition.signalAll();
} catch (Exception e) {
}finally{
lock.unlock();
}
}
public void methodTwo(){
lock.lock();
try {
condition.await();
System.out.println("方法二执行.......");
} catch (Exception e) {
//TODO: handle exception
}finally{
lock.unlock();
}
}
}
复制代码
public static void main(String[] args) {
ExecutorService ThreadPool = Executors.newCachedThreadPool();
AWaitTest aWaitTest = new AWaitTest();
ThreadPool.execute(() -> aWaitTest.methodTwo());
ThreadPool.execute(() -> aWaitTest.methodOne());
}
复制代码
方法一执行......
方法二执行.......
复制代码
这种方法很明显wait()那一套更加灵活
线程池
说到线程池,我们不得不提一下线程池的七大参数了。
线程池七大参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
-
corePoolSize 核心线程数
当提交一个任务时,线程池创建一个新的线程来执行任务,直到线程数量达到corePoolSize的时候 这个时候会将线程放入到工作队列当中阻塞 复制代码
-
maximumPoolSize 最大线程数
这个参数只在,工作队列是有边界的时候生效,如果工作队列没有边界,这个参数将不生效 因为会将新的线程一直添加到工作队列当中 复制代码
-
keepAliceTime 线程空闲的存活时间
因为线程执行任务执行完毕之后不会立即死亡,会继续存活下来,而这个参数就是在限制线程的存活时间 还有一点需要注意,这个参数默认只在当前线程数大于核心线程数的情况下生效 复制代码
-
unit 线程存活时间单位
见名知意,这个参数作为keepAliceTime的单位 复制代码
-
workQueue 工作队列
用于保存等待需要执行任务的新线程 复制代码
-
threadFactory 线程工厂
用于创建线程的线程工厂 复制代码
-
handler 饱和策略
当阻塞队列满了,并且没有空闲的的工作线程,如果此时还不断提交任务,线程池必须进行处理 线程池提供了四种饱和策略 1. AbortPolicy: 直接抛出异常,默认策略 2. CallerRunsPolicy: 用调用者所在的线程来执行任务 3. DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务 4. DiscardPolicy: 直接丢弃任务 复制代码
线程池的执行流程
线程池到底是怎么来创建线程的呢?
我们在提交任务后,
线程池会先判断当前线程数是否大于核心线程数,
如果不大于则直接创建工作线程,
否则则创建线程添加到阻塞队列当中,
然后,如果线程池会再次进行判断阻塞队列是否满
如果不满,则直接添加到阻塞队列当中
否则,会再次判断当前线程数是否大于最大线程数
如果大于则执行拒绝策略,否则就创建线程
复制代码