一,线程的概念
1.1,线程是什么?
一个线程就是一个 "执行流"(execution stream)。每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码。简单来说,就是做一件事情的相关流程。 比如:辅导员分发给班长一系列任务,需要打扫卫生,管理纪律和收团课等,只有班长一个人是肯定忙不过来的,于是班长叫来了劳动委员,纪律委员和团支书等来协助班长完成这一系列任务,因此就有了四个执行流一起完成任务,但他们本质上都是为辅导员做事。
此时,就叫多线程,把大任务划分成小任务交给不同的执行流分别完成,劳动委员,纪律委员和团支书都是班长叫来的,因此班长被称为主线流(Main Thread)
每个线程都是一个独立的执行流
多个线程都是并发执行的
1.2,为啥要有线程?
首先,因为CPU的发展达到瓶颈期,要想提高算力就需要多核CPU,然而并发编程能充分的利用好多核CPU。
其次,线程的创建,销毁和调度均比进程快。
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程" (Coroutine)
二,线程的创建
2.1,如何创建线程?
方法一:继承Thread类
(1) 继承 Thread 来创建一个线程类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello");
}
}
(2) 创建 MyThread 类的实例
MyThread t = new MyThread()
(3) 调用 start 方法启动线程
t.start();
方法二:实现Runnable接口
(1) 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
(2) 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());
(3) 调用 start 方法
t.start();
2.2,一个简单的多线程程序
import java.util.Random;
public class ThreadDemo {
// 定义一个继承自Thread的内部类
private static class MyThread extends Thread {
// 重写run方法,在该方法中定义线程的执行逻辑
@Override
public void run() {
Random random = new Random();
// 使用无限循环使线程不断执行任务
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停止运行 0-9 秒
int sleepTime = random.nextInt(10); // 生成0到9之间的随机数作为线程睡眠的时间
Thread.sleep(sleepTime * 1000); // 将秒转换为毫秒,并让线程睡眠指定的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 创建三个线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
// 启动三个线程
t1.start();
t2.start();
t3.start();
Random random = new Random();
// 使用无限循环使主线程不断执行任务
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停止运行 0-9 秒
int sleepTime = random.nextInt(10); // 生成0到9之间的随机数作为线程睡眠的时间
Thread.sleep(sleepTime * 1000); // 将秒转换为毫秒,并让主线程睡眠指定的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如果想观察线程的运行状态可以命令框中输入jconsole命令,如图:
三,线程的状态
线程的状态请移步另外一篇文章:https://mp.csdn.net/mp_blog/creation/editor/132074565
四,多线程带来的风险
1.线程安全问题:在多线程环境中,多个线程可能同时访问和修改相同的数据,这可能会导致数据不一致和不可预测的结果。需要通过同步机制来保证数据的一致性和线程安全。
2.性能问题:线程上下文切换,带来一定的性能损耗。
3.活跃性问题:死锁,饥饿,活锁。死锁是两个或者两个以上线程在争夺资源造成的
五,线程相关关键字
5.1 synchronized关键字
5.1.1synchronized关键字的特性
1)互斥:synchronized关键字会起到互斥效果,某个线程执行到某个对象synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。进入synchronized修饰的代码块相当于加锁。退出synchronized修饰的代码块相当于解锁。例如:
synchronized用的锁是存在Java对象头里的,一个对象上了锁,其他线程就只能等待这个线程释放 。synchronized的底层是使用操作系统mutex lock实现的。
(2)刷新内存
synchronized的工作过程:1.获得互斥锁 2.从主内存拷贝变脸的最新副本到工作的内存 3.执行代码 4.将更改后的共享变量的值刷新到主内存 5.释放互斥锁。
(3)可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死(第一次加锁,加锁成功,第二次加锁,锁已经被占用,阻塞等待)的问题。
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待。直到第一次的锁被释放,才能获取到第二个锁。但是释放第一个锁也是由该线程完成,该线程无法进行解锁操作,这个时候就会产生死锁。
在上面代码中, increase和increase1这两个方法都加了锁,此处的synchronized都是针对this当前对象加锁的。在调用increase1的时候先加了一次锁,执行到increase的时候,又加了一次锁(上个锁还没释放,等于加了两次锁),代码没有问题,因为synchronized是可重入锁。
在可重入锁中,包含了"线程持有者"(当前的this对象)和“计数器”这两个信息
如果某个线程加锁的时候,发现锁已经被占用,但是占用的恰好是自己,那木仍然可以继续获取到锁,并让计数器自增。
解锁的时候计数器递减为0的时候,才真正释放锁。(才能被别的线程获取到)
5.1.2 synchronized使用相关示例
1)直接修饰普通方法:锁的Synchronized对象
public class Synchronized{
public synchronized void methond(){
}
}
2) 修饰静态方法:锁的Synchronized类的对象
public class Synchronized{
public synchronized static void method(){
}
}
3)修饰代码块:明确指定锁哪个对象.
锁当前对象
public class Synchronized{
public void method(){
synchronized(this){
}
}
}
锁类对象
public class Synchronized{
public viod method(){
synchronized(Synchronized.class){
}
}
}
5.2 volatile关键字
5.2.1 volatile能保证内存可见性
volatile关键字修饰的变量,能保证“内存可见性”
代码在写入volatile修饰变量的时候会改变线程工作中volatile变量副本的值,然后将改变后副本的值从工作内存刷新到主内存。
代码在读取volatile修饰的变量的时候会从主内存中读取volatile变量的最新值到线程的工作内存中,从工作内存中读取volatile变量的副本。
例如:
在未加volatile时t1感知不到flag的变化!!!
在上述代码中:创建两个线程t1和t2,t1中包含一个循环,这个循环以flag==0为条件,
t2中从键盘读入一个整数,并把这个整数复制给flag,预取当用户输入非零的值的时候t1线程结束。
5.2.2 volatile不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性。
例如:给increase方法去掉synchronized,给count加上volatile关键字
class Demo2 {
volatile public int count=0;
void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException{
final Demo2 counter = new Demo2();
Thread t1 = new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
可以看到最终count的值仍然无法保证是100000
5.3 synchronized也能保证内存可见性
synchronized既然能保证原子性,也能保证内存可见性
对上面代码进行调整:去掉volatile,给t1的循环内部加上synchronized,并借助counter加锁对象
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
六,线程相关方法、
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知,但是在实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序】
完成这个协调工作,主要涉及到三个方法
*wait()/wait(long timeout);让当前线程进入等待状态
*notify()/notifyAll()唤醒在当前线程上等待的线程
*wait,notify,notifyAll都是Object类的方法
6.1. wait()方法
wait要做的事:
wait会使当前执行的代码进行等待(把线程放到等待队列中);
释放当前的锁;
满足一定条件时被唤醒,重新尝试获取这个锁。
wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait结束等待的条件:
其他线程调用该对象的notify方法
wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定的等待时间)
其他线程调用该线程等待线程的interrupted方法,导致wait抛出InterruptedException异常
示例:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object){
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait()之后就一直等待下去,这个时候就需要使用到一个唤醒的方法notify()。
6.2 notify()方法
notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程。
1. notify()也要在同步方法或者同步块中调用,该方法是用来通知那些可能等待的对象。
2.如果有多个线程在等待,则线程调度器会随机挑选一个呈现wait()状态的线程(并不存在先来后到)。
3.当notify()方法结束后,线程不会立即释放该对象锁,要等到执行notify()方法的线程将程序执行完毕,也就是同步代码块结束之后才会释放该对象的锁 。
例如:
public class Main implements Runnable{
private Object locker;
public Main(Object locker){
this.locker=locker;
}
@Override
public void run() {
synchronized (locker){
while(true){
try{
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new Main(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
6.3 notifyAll()方法
notifyAll方法虽然能同时唤醒多个等待的线程,但是这三个线程是需要通过锁竞争而不是同时执行。
例如:
public class Main implements Runnable{
private Object locker;
public Main(Object locker){
this.locker=locker;
}
public static void main(String[]args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new Main(locker));
Thread t3 = new Thread(new Main(locker));
Thread t4 = new Thread(new Main(locker));
Thread t2 = new Thread(new Main(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
@Override
public void run() {
synchronized (locker){
System.out.println("notifyAll,begin!!!");
locker.notifyAll();
System.out.println("notifyAll,over!!!");
}
}
}
6.4 wait和sleep的对比
wait是用于线程之间的通信,sleep是让线程阻塞一段时间。
wait需要搭配synchronized使用,sleep不需要。
wait是Obiect的方法,sleep是Thread的静态方法。
唯一的相同点就是都可以让线程放弃执行一段时间。
七,相关案例
7.1 单例模式
跳转到另外一篇 http://t.csdnimg.cn/NGANP
7.2 阻塞式队列
阻塞队列是什么?
阻塞队列是一种特殊的队列,遵循先进先出的原则。
当队列满了的时候,继续入队就会阻塞,直到其他线程从该队列中取走元素。
当队列空了的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。
阻塞队列的典型的案例就是“生产者消费者模型”
生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。(强耦合:模块之间联系越多,耦合性越强,其独立性越差)
生产者和消费者之间不直接通讯,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接丢给阻塞队列,消费者直接从阻塞队列中拿区数据。
1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者之间的处理能力。
2)阻塞队列也能使生产者和消费者之间解耦合
八,线程池
线程池就是线程的创建和销毁过程
线程池的最大好处就是减少每次启动,销毁线程的损耗。
8.1 标准库中的线程池:
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
8.2 Executors创建线程池的几种方法
1)newFixedThreadPool:创建固定线程数的线程池
2)newCachedThreadPool:创建线程数目动态增长的线程池
3)newSingThreadExecutor:创建只包含单个线程的线程池
4)newScheduledThreadPool:设定延迟实践后执行命令,或者定期执行命令
8.3 实现线程池
1)核心操作为submit,将任务加入线程池中
2)用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务
3) 使用一个 BlockingQueue 组织所有的任务
4)每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
5)指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增 线程了.
class Worker extends Thread {
private LinkedBlockingQueue<Runnable> queue = null;
public Worker(LinkedBlockingQueue<Runnable> queue) {
super("worker");
this.queue = queue;
}
@Override
public void run() {
// try 必须放在 while 外头, 或者 while 里头应该影响不大
try {
while (!Thread.interrupted()) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
}
}
}
public class MyThreadPool {
private int maxWorkerCount = 10;
private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
public void submit(Runnable command) {
if (workerList.size() < maxWorkerCount) {
// 当前 worker 数不足, 就继续创建 worker
Worker worker = new Worker(queue);
worker.start();
}
// 将任务添加到任务队列中
queue.put(command);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool();
myThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("吃饭");
}
});
Thread.sleep(1000);
}
}
九,线程和进程的异同点
1.进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位
2.进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
3.由于同一进程的各线程间共享内存和文件资源,可以不通过内核直接通信。
4.线程的创建,切换以及终止的效率更高。