-
文章目录
-
线程和进程
- 并发和并行
- 线程的状态
- 线程的构造方法和属性
- 创建线程的方式
-
线程安全问题
-
如何解决线程安全
-
单例模式
- 饿汉模式
- 懒汉模式
-
阻塞队列
-
定时器
线程和进程
- 进程:进程是程序的一次执行过程,是资源分配的最小单位
- 线程:是轻量级进程,是作业分配的最小单位
- 关系:一个程序运行起来至少有一个进程(对于Java来说,程序执行起来后main()是必须执行的),一个进程中可以包含多个进程
并发和并行
-
并发:指的是两个或者多个事件在同一时间段内发生
-
并行:值得是两个或者多个事件在同一时刻发生
在操作系统中,如果安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,在单核 CPU 中,每一时刻只能有一道程序执行,也就是微观上这些程序是分时的交替运行,只不过在宏观上感觉是同时运行;
而在多核 CPU 系统中,这些程序可以并发执行并且可以分配到多个处理器(CPU)上,实现多任务并行执行,也就是利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。
线程的状态 -
新建状态(New)
此时线程已经有了相应的内存空间和其它资源,但是还没有开始执行。 -
就绪状态(Runnable)
新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态。
由于还没有分配CPU,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。当系统挑选一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态。系统挑选的动作称之为“CPU调度"。一旦获得CPU线程就进入运行状态并自动调用自己的run方法。 -
运行状态(Running)
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。
run()方法定义了该线程的操作和功能。运行状态中的线程执行自己的run方法中代码,直到调用其他方法或者发生阻塞而终止。 -
阻塞状态(Blocked\waitting\TimedWaiting)
一个正在执行的线程在某些特殊情况下,如被人为挂起或I/O请求,将让出 CPU 并暂时中止。
自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()、 suspend()、 wait()等方法,线程都将进入堵塞状态。
堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程转入就绪状态。重新到就绪队列中排队等待,这时被CPU调度选中后会从原来停止的位置开始继续执行。
注意:阻塞被消除后是回到就绪状态,不是运行状态。 -
死亡状态(Teminated)
线程调用 stop()方法、destory()方法或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。![在这里 -
线程的构造方法和属性
- 常见构造方法
方法 | 说明 |
---|---|
Thread | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并且命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并且命名 |
- 常见属性
属性 | 获取方法 |
---|---|
Id | getId( ) |
名称 | getName( ) |
状态 | getState( ) |
优先级 | getPriority( ) |
是否后台线程 | getDaemo( ) |
是否存活 | isAlive( ) |
是否被中断 | isInterruptted( ) |
属性说明:
(1)Id:线程的唯一标识,不会重复
(2)名称在调试的时候便于观察线程的活动
(3)状态表示线程当前所处的一个情况
(4)优先级高的线程理论上来说更容易被调度到
(5)关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
(6)是否存活,即简单的理解,为 run 方法是否运行结束了
-
创建线程的方式
- 继承Thread类
public class MyThread extends Thread {
//run方法中的代码块是线程调用start方法获得CPU时执行的语句
@Override
public void run() {
System.out.println("我是继承Thread的线程");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("我是实现Runnable接口的新线程");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
- 匿名内部类
public class ThreadDemo {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
System.out.println("我是线程1");
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
System.out.println("我是线程2");
}
};
t2.start();
}
}
- 线程安全问题
在多线程并发运行的过程中,会涉及到调度,线程之间抢占CPU等,这些问题就会致使程序运行的结果与我们预期结果不相符合,于是乎线程安全问题就来了!导致线程安全问题,我归纳了如下几点:
(1)抢占式执行方式(线程不安全的万恶之源)
线程间的调度完全是由内核负责,现成的先后执行顺序用户无法控制和感知
(2)操作非原子性
例如经典的自增操作,有三个步骤:首先将数据从内存读取到CPU的寄存器上,对数据进行自增操作(保存在寄存器中)
最后将寄存器的数据写回内存,期间的操作如果是并发执行,就可能出现差错
(3)多个线程尝试修改同一个变量
一个线程可以修改一个变量,多个线程尝试读取同一个变量也没问题,多个线程修改多个变量也没问题
(4)内存可见性
(5)指令重排序
代码在编译过程中,会被优化(编译器在保证不改变逻辑的情况下会调节指令顺序,提高代码的执行效率)
既然会出现线程不安全,会影响程序执行结果,那么作为程序媛我们 当然就会想办法对以上问题提出解决措施!
解决线程安全问题
- 针对抢占式执行,由于线程在抢占CPU时是在操作系统内核进行的,这个就无能为力啦(#.#)
- 针对操作非原子性,这个我们就提出了一个关键字Synchronized,采用加锁方式去解决线程不安全,举个简单例子:
加锁前:
/**
* 这里我假设两个线程各自自增10000次
* 预期结果是它们自增的总次数应该是20000
* */
public class ThreadDemo2 {
static class Counter{
public int count;
public void increase(){
count++;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++) {
counter.increase();
}
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++) {
counter.increase();
}
}
};
t2.start();
System.out.println(counter.count);
}
}
运行结果:11535
加锁后:
public class ThreadDemo2 {
static class Counter{
public int count;
synchronized public void increase(){
count++;
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++) {
counter.increase();
}
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++) {
counter.increase();
}
}
};
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
运行结果:20000
锁的特点:锁是互斥的,一时刻只有一个进程可以获取到锁,如果其他进程也在竞争这个锁,就会发生阻塞等待;当且仅当第一个锁释放后,其他进程才能竞争锁
使用锁的方式:
(1)Synchronized加在普通方法前,相当于给this加锁
(2)Synchronized加在静态方法前,相当于给类对象(JVM在运行时将.class文件加载到内存获取到的)加锁
(3)Synchronized加到某个代码块前,相当于显示指定给某个对象加锁
综上所述:Synchronized的使用场景是多个线程尝试修改同一个变量
- 针对内存可见性,使用关键字Volatile(只能解决一个线程进行读操作,一个线程进行写操作)方式编译器过度优化,这里就不详细介绍,下面单利模式中的懒汉模式会提到
-
单例模式
单例模式是一种常见的设计模式,应用的场景是代码中有些概念不应该存在多个实例(比如说数据库连接池中的DataSource);- 懒汉模式:在类加载过程中就实例化对象,线程安全
public class ThreadDemo3 {
static class SingleTon{
//将SingleTon的无参构造方法设置为private,这样它就只能在类内实例对象,不支持类外实例化对象
private SingleTon(){
}
//将SingleTon实例对象设置为static,这样就仅有一份实例且不依赖于对象,仅与类有关
private static SingleTon instance = new SingleTon();
//获取SingleTon实例的方法
public static SingleTon getInstance(){
return instance;
}
}
public static void main(String[] args) {
SingleTon s = SingleTon.getInstance();
}
}
- 懒汉模式:类加载过程没有初始化,第一次调用getInstance方法的时候初始化,由于操作非原子性,线程不安全
public class ThreadDemo5 {
static class SingleTon{
//将SingleTon的无参构造方法设置为private,这样它就只能在类内实例对象,不支持类外实例化对象
private SingleTon(){
}
//还有一个问题,就是线程在实例化的时候编译器会自动优化,在此过程中也可能会出现现场不安全问题
//在下面实例化SingleTon的步骤存在读操作和修改操作,因此需要使用Volatile关键字来防止过度优化造成线程不安全
private volatile static SingleTon instance = null;
public static SingleTon getInstance(){
//这里存在一个线程安全问题,操作非原子性,此时我们就需要用到Synchronized关键字
//假设存在两个线程在(2)(3)步骤中就会违背单例模式的意图,两个对象就实例对象2次,线程越多,实例的对象越多
//实例化SingleTon的步骤:
//(1)读取当前SingleTon对象
//(2)判断SingleTon对象是否为null
//(3)如果是null就new SingleTon
//(4)返回结果
//第一次if循环的作用是减少锁的开销,只有在第一次实例化时才会加锁
if(instance == null){
synchronized (SingleTon.class){
if (instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
}
public static void main(String[] args) {
SingleTon s = SingleTon.getInstance();
}
}
- 阻塞队列
特性:
①具备两个方法,入队列和出队列
②入队列时如果队列满了就会被阻塞,直到其他线程有元素出队列时才可以继续出队列操作;
③出队列操作时如果队列空了就会被阻塞,直到有其他线程有其他元素入队列才可以继续进行出队列操作;
可以用于模拟实现生产者消费者问题
public class ThreadDemo {
static class BlockingQueue {
private int head = 0;
private int tail = 0;
//此处的size设计要读取判断和修改,可能会出错,因此要加上volatile关键字
private volatile int size = 0;
private int[] array = new int[1000];
public void put(int value) {
//由于此处的修改操作非原子性,那么就需要给当前对象加锁
synchronized (this) {
//判断当前队列是不是满了,若队列已满就阻塞
while (tail == array.length) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
array[tail] = value;
tail++;
if (tail == array.length) {
tail = 0;
}
size++;
//已经完成插入操作,可以唤醒线程(这里是唤醒出队列线程)
notifyAll();
}
}
}
public int take() {
int ret = -1;
//由于此处的修改操作非原子性,那么就需要给当前对象加锁
synchronized (this) {
//判断当前队列是不是为空,如果为空就阻塞
while (size == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ret = array[head];
head++;
if (head == array.length) {
head = 0;
}
//出队列操作已完成,可以唤醒线程(这里是唤醒入队列线程)
size--;
notifyAll();
}
return ret;
}
}
//模拟生产者消费者
public static void main(String[] args) {
BlockingQueue queue = new BlockingQueue();
Thread producer = new Thread(){
public void run(){
for (int i = 0; i < 10000; i++) {
queue.put(i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者生产元素:"+i);
}
}
};
producer.start();
Thread costumer = new Thread(){
public void run(){
while(true){
int ret = queue.take();
System.out.println("消费者消费元素:"+ret);
}
}
};
costumer.start();
}
}
}
- 定时器
定时器就像一个极其灵敏的闹钟,时间非常精确甚至可以达到微秒毫秒级,经常用于Internet
定时器的组成:
①有一个Task类,负责描述任务逻辑,规定任务的执行时间
②有一个Timer类,用阻塞优先队列来组织Task对象
③有一个扫描线程,来循环检测判断当前进程是否可以执行任务
④有一个schedule方法负责安排任务,让调用者调用
public class ThreadDemo2 {
//此处的Task描述的是一个一段要执行的任务逻辑,由于一会儿要在优先级阻塞队列中使用
//所以Task类要可比较
static class Task implements Comparable<Task>{
//Runnable中有一个run()用来描述要执行任务的内容
private Runnable command;
//执行任务的时间,此处是一个相对时间
private long time;
public Task(Runnable command,long after){
this.command = command;
this.time = System.currentTimeMillis()+after;
}
@Override
public int compareTo(Task o) {
return (int) (this.time-o.time);
}
public void run(){
command.run();
}
}
//扫描线程类
static class Worker extends Thread{
private PriorityBlockingQueue<Task> queue = null;
private Object mailBox = null;
public Worker(PriorityBlockingQueue<Task> queue,Object mailBox){
this.queue = queue;
this.mailBox = mailBox;
}
public void run(){
while(true){
//取出队首元素,判断是否时间到了
try {
Task task = queue.take();
//检查当前任务时间是否到了
long curTime = System.currentTimeMillis();
//任务时间大于当前时间,证明时机未到,就把任务塞回优先级队列
//此处有可能出现"忙等现象",CPU一直在不停地比较执行任务时间和curTime,造成资源浪费
if (task.time > curTime){
queue.put(task);
synchronized (mailBox){
mailBox.wait(task.time - curTime);
}
}else {
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
static class Timer{
//为了避免忙等现象,需要借用辅助对象
private Object mailBox = new Object();
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
public Timer(){
//创建线程
Worker worker = new Worker(queue,mailBox);
worker.start();
}
//提供方法,安排任务,让调用者调用
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
queue.put(task);
synchronized (mailBox){
mailBox.notify();
}
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hehe");
timer.schedule(this,1000);
}
},1000);
}
}