多线程学习(基础篇)
Posted 3 ERROR(s)
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程学习(基础篇)相关的知识,希望对你有一定的参考价值。
文章目录
一、多线程概述
为什么要有线程呢?
首先并发已经成为了现在变成的刚需,虽然进程也可以实现并发编程,但是线程相比于进程更清轻量,它创建,销毁,调度线程都要比进程快。
1.进程和线程之间的关系
- 进程是一个应用程序,线程是一个进程中的执行单元。
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 一个进程可以启动多个线程,同一个进程中的多个线程之间可能共享了一些资源。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
在java语言中,线程A和线程B的堆内存和方法区内存是共享的,栈内存独立不共享,一个线程一个栈。,之所以引进多线程机制,就是为了提高程序的处理效率。
2.创建线程的方式
方式一:显式继承Thread,重写run方法来指定线程的执行代码。
public class ThreadDemo5
public static void main(String[] args)
myThread t=new myThread();//创建myThread实例
t.start();
class myThread extends Thread
@Override
public void run()
System.out.println("多线程分支");
方式二:匿名内部类继承Thread,重写run方法来指定线程的执行代码。
Thread t1=new Thread()
@Override
public void run()
int a=0;
for (int i = 0; i < count; i++)
a++;
;
方式三:显式实现Runnable接口,重写run方法。
class myRunable implements Runnable
@Override
public void run()
System.out.println("多线程分支2");
创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
方式四:匿名内部类创建 Runnable 子类对象
Thread tttt =new Thread(new Runnable()
@Override
public void run()
System.out.println("匿名内部类Runnable创建 Thread 子类对象");
);
3.Thread的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
4.中断一个线程
通过共享的标记来进行
public class ThreadDemo3
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException
//温和版本
Thread t = new Thread()
@Override
public void run()
while(!isQuit)
System.out.println("正在嘟嘟嘟");
try
Thread.sleep(300);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("结束了");
;
t.start();
Thread.sleep(5000);
System.out.println("终止交易");
isQuit=true;
调用 interrupt() 方法
public class ThreadDemo4
public static void main(String[] args) throws InterruptedException
//暴力版本
Thread t =new Thread()
@Override
public void run()
while(!Thread.currentThread().isInterrupted())
System.out.println("正在嘟嘟嘟");
try
Thread.sleep(300);
catch (InterruptedException e)
e.printStackTrace();
break;
System.out.println("嘟嘟嘟结束了");
;
t.start();
Thread.sleep(1000);
System.out.println("终止交易");
t.interrupt();
- Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
5.获取当前线程引用
线程默认命名规则为:Thread-0;Thread-1…
获取对象信息包括:获取线程对象,获取线程名字,修改对象名字。
public class ThreadDemo6
public static void main(String[] args)
Thread tt=Thread.currentThread();
System.out.println(tt.getName());
tt.setName("HEHE");
System.out.println(tt.getName());
获取线程对象的方法使用currentThread(),这是一个静态方法,作用是获取当前对象的引用。若此方法在main中,只会现实除main中的线程,即主线程,其名字就叫做"main"
获取线程名字,修改对象名字:
public class ThraeadDemo6
public static void main(String[] args)
Thread t=new Thread();
System.out.println(t.getName());
t.setName("HEHE");
System.out.println(t.getName());
6.线程的状态
- NEW: Thread对象有了,内核中的线程(PCB)还没有。安排了工作, 还未开始行动
- RUNNABLE: 就绪状态,可工作的,当先线程在CPU上或者随时上CPU,有一个专门的就绪队列来维护。
- BLOCKED: 阻塞状态 这几个都表示排队等着其他事情
- WAITING: wait 这几个都表示排队等着其他事情
- TIMED_WAITING: 超时等待
- TERMINATED: 内核中的线程已经结束了(PCB没了),但是Thread对象还在(需要GC来回收)
isAlive表示线程存活,除了NEW和TERMINATED之外其余状态都是isAlive。
二、线程安全
1.演示线程不安全
/*
* 测试线程安全
*
*/
public class ThreadDemo7
public static class Counter
public int count=0;
public void increase()
count++;
public static void main(String[] args) throws InterruptedException
Counter counter=new Counter();
Thread t=new Thread()
@Override
public void run()
for (int i = 0; i < 50000; i++)
counter.increase();
;
t.start();
Thread tt=new Thread()
@Override
public void run()
for (int i = 0; i < 50000; i++)
counter.increase();
;
tt.start();
t.join();
tt.join();
System.out.println(counter.count);
正常来说结果应该是100000,但是测试之后发现结果如下:
我们发现每次测试结果都不相同且并不是100000,原因如下
- 线程是抢占式调度
- 自增操作不是原子的(1.把内存的数据独到CPU。2.把CPU当中的数据自增一。3.再把CPU当中的数据写回到内存中。)
- 多个线程尝试修改同一个变量。
- 内存可见性导致的线程安全问题
- 指令重排序
2.如何避免线程安全问题
对症下药!
(1).抢占式调度(这个没办法解决,操作系统内核实现)
(2).自增操作非原子性(给自增操作加锁)
这里加上关键字synchronized加锁,加锁之后同一时刻只有一个线程能获得锁,如果其他线程也尝试获得锁,就会陷入阻塞状态,直到刚才的锁释放,此时剩下的线程再重新竞争锁,也就是说加了锁把原本并行的线程强制改成了串行,再次运行后结果为100000。
3.synchronized关键字
synchronized关键字:进入方法前先尝试加锁,方法结束后自动解锁,这样的好处就是避免忘记解锁的情况。如果加锁的时候锁被占用了,该代码就会阻塞等待,直到前面的锁被释放,才能获取到这个锁。
synchronized的几种常见用法:
- 加在普通方法前:表示锁this。
- 加到静态方法前:表示锁当前类的类对象。
- 加到某个代码块之前:显示指定给某个对象加锁。
特性1:互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
特性2:刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
特性3:可重入
按照之前的设定,只有当第一个锁被释放之后才能获得第二个锁,但是释放第一个锁是由第一个线程来完成的,如果第一个线程开始摆烂,那么这个锁将无法打开,也就造成了死锁。
这样的锁被称为不可重入锁
而synchronized是可重入锁在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁。
理解锁具体细节:
- 工作流程:多个线程同时获取同一把锁只有一个线程可以获取到,其他线程会阻塞等待(BLOCKDE),两个线程分别尝试获取两把不同的锁, 不会产生竞争。
- 底层实现:每个对象都有一个对象头,头里面有一个锁标记,所以它势必要搭配一个具体的对象来使用
- 加锁的时候一定要明确到底是给哪个对象加锁。
4.Java 标准库中的线程安全类
这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
以下为加了锁的线程安全的类
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
三、volatile关键字
写入volatile修饰的变量的时候
- 改变线程CPU寄存器中volatile变量副本的值。
- 将改变之后副本的值从CPU寄存器中刷新到主内存中。
读取volatile修饰的变量的时候
- 从主内存中读取volatile修饰的变量最新值到CPU寄存器当中。
- 从CPU寄存中读取volatile变量的副本。
访问工作内存(CPU寄存器或者CPU缓存)的速度远远快于访问主内存,但是有可能数据不一致,而加上了volatile之后强制读写主内存,虽然速度慢了但是不会出错。
代码示例:
/**
* @ClassName UseVolatile
* @Description 这是用来测试volatile关键字的测试代码
* 正常情况下线程2输入数据完毕之后线程1应该结束,但是实际情况是这样嘛?
* @Author Rcy
* @Data 2022/1/11 0:37
*/
public class UseVolatile
static class A
public int flg=0;
public static void main(String[] args)
A a=new A();
Thread thread=new Thread()
@Override
public void run()
while(a.flg==0)
System.out.println("结束了");
;
thread.start();
Thread thread2=new Thread()
@Override
public void run()
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个数字:");
a.flg=sc.nextInt();
;
thread2.start();
我们测试了很多此发现此时线程1并没有结束!这是因为线程1当中的flag读取的一直都是CPU寄存器当中的值,我们的线程二将主内存当中的值修改了并没有影响到线程1。这是系统优化的负面效果,我们要消除这种优化,所以我们想到了使用volatile关键字修饰flag让他每次从内存中读取数值,保证了内存可见性 (一个线程读,一个线程写,可能修改操作对于读线程没有生效)。
修改后的代码如下:
static class A
public volatile int flg=0;
测试结果如下:
volatile虽然能保证内存可见性,但是它不能保证原子性。
四、wait()和notify()方法
1.wait() 方法:
作用:
- (1)让其加入等待队列,释放当前的锁
- (2) 等待接收通知
- (3)收到唤醒通知,从新尝试获取锁
如果notify()发送通知在1,2之间,可能会出现竞态条件问题(错过了通知,导致登了一辈子也没等明白),所以wait()中的1,2是原子性的。
wati()结束条件:
- 其他线程调用该对象的notify()方法
- wait等待超时(timeout参数控制)
- 其他线程调用该等待线程的interrupted方法
这里需要注意的是,如果被interrupted的线程没有start则等待状态不会恢复
2.notify()方法:
- 通知等待该对象对象锁的其他线程,对其发出通知,让他们从新获得该对象锁。
- 若有多个线程等待随机选一个。
- 不会立即释放该对象锁 ,必须等到同步代码块执行完。
3.为什么wati() 方法和notify()方法需要synchronized一起使用?
防止错过notify()发送的通知,造成永远阻塞等待。
每一个对象都有一个监视器,监视器当中有有一个锁和一个阻塞队列和同步队列,因为wait()阻塞的线程放在阻塞队列中,因为竞争失败则放在同步队列当中,notify()则是把阻塞队列的线程放到同步队列当中去。
4.wait()和sleep()方法比较
相同点:
- 都可以让线程阻塞一段时间
不同点:
- wait()需要搭配synchronized使用,sleep不需要。
- wait()是Object的方法,sleep()是Thired的静态方法。
五、单例模式讲解
之前专门写过一篇介绍单例模式模式的文章:
六、阻塞队列
阻塞队列是什么?
阻塞队列是一种特殊的队列,也遵循先进先出的的原则,它是一种线程安全的数据结构,他有以下一些特点:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
- 当队列空的时候,继续出队列就会阻塞,知道有其他线程往队列中插入元素。
1.借助标准库实现生产者消费者模型
生产者消费者模型就是通过一个容器解决生产者和消费者之间的强耦合问题。
生产者和消费者之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产出来数据之后不需要等消费者处理,直接丢给阻塞队列,消费者也不问生产者要东西,直接在阻塞队列中取!
- BlockingQueue实际是一个接口,具体实现的类是LinkedBlockingQueue。
- put用于阻塞式入队列,take用于阻塞式出队列。
- BlockingQueue也有offer,poll,peek方法,但是不带阻塞性质。
/**
* @ClassName ThreadQueue
* @Description 标准库版生产者消费者模型
* @Author Rcy
* @Data 2022/3/8 16:28
*/
public class ThreadQueue
public static void main(String[] args) throws InterruptedException
BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
//put和offer都可以入队,put有阻塞的作用
//生产者
Thread producer = new Thread()
@Override
public void run()
for (int i = 0; i <10000 ; i++)
try
blockingQueue.put(i);
System.out.println("生产了元素"+i);
sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
;
producer.start();
//消费者
Thread customer = new Thread()
@Override
public void run()
while(true)
try
Integer cur= blockingQueue.take();
System.out.println("消费元素"+cur);
catch (InterruptedException e)
e.printStackTrace();
;
customer.start();
customer.join();
producer.join();
2.阻塞队列的实现
- 使用循环队列的方式来实现阻塞队列
- 使用synchronized关键字进行加锁控制
put唤醒take的阻塞,take唤醒put的阻塞
put插入元素,如果队列满了就要进入wait等待(由take方法来唤醒) (这里注意要在while中等待,因为被唤醒的时候有可能同时唤醒了多个线程,这里要用while再次进行判断)
take取出元素的时候,判断队列是否为空,为空就进行wait等待 (在while中进行等待,原因同上)
import java.util.Queue;
/**
* @ClassName BlockingQueue
* @Description TODO
* @Author Rcy
* @Data 2022/3/8 18:05
*/
public class WriteBlockingQueue
static class BlockingQueue
private int[] elem = new int[1000];
int start = 0;
int tail = 0;
int size = 0;
private void put(int item) throws InterruptedException
//输入队列满了 就要开始阻塞了
synchronized (this)
if(elem.length==size)
this.wait();
//入队列,把新的元素放到数组的尾部
elem[tail]=item;
//数组尾部++
tail++;
//如果超过或等于尾部就从0开始
while(tail>=elem.length)
tail = 0;
size++;
this.notify();
//出队列
private int take() throws InterruptedException
//要阻塞了
int ret=0;
synchronized (this)
while(size==0)
this.wait();
ret = elem[start];
//相当于出队列
start++;
//判断当前start大小超过或等于就从0开始计数
if(start>=elem.length)
start=0;
size--;
this.notify();
return ret;
多线程学习(基础篇)