来 多线程,一个学起来挺难但是实际应用不难的一个知识点,甚至在很多情况下都不需要考虑,最多就是写测试类的时候模拟一下并发,现在我们就来讲讲基础的多线程知识。
一、线程和进程、并发与并行
1.1、线程和进程
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是一个进程中的实际执行单位,它负责当前进程中程序的执行。在一个进程中可以有多个线程,这些线程可以共享进程的资源,如堆和方法区。然而,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。因此,系统在产生一个线程或在不同线程间切换时的负担要小于进程,这也使得线程被称为轻量级进程
进程:进程是程序的基本执行实体。一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
1.2、并发与并行
并发:在同一时刻,有多个指令在单个CPU上交替执行 。
并行:在同一时刻,有多个指令在多个CPU上同时执行
二、实现多线程的三种方法
2.1 继承Thread
多线程的第一种启动方式:
- 自己定义一个类继承Thread
- 重写run方法
-
public class Student extends Thread{ @Override public void run() { // getName():获取当前线程名字 for(int i=0;i<200;i++) { System.out.println("我是学生"+getName()); } } }
-
- 创建子类的对象,并启动线程
-
@Test void threadtest1(){ Student s1 = new Student(); Student s2 = new Student(); s1.setName("1");//设置线程名 s2.setName("2");//设置线程名 s1.start(); //启动线程用start 而不是调用run方法 s2.start(); }
-
来看结果:
2.2、实现Runnable接口
多线程的第二种启动方式:
- 自己定义一个类实现Runnable接口
- 重写里面的run方法
-
public class Student implements Runnable{ @Override public void run() { // getName():获取当前线程名字 for(int i=0;i<200;i++) { //Thread.currentThread():获取当前执行该线程的线程对象 Thread thread = Thread.currentThread(); System.out.println("我是老师"+thread.getName()); } } }
-
- 创建自己的类的对象
- 创建一个Thread类的对象,并开启线程
-
@Test void threadtest2(){ Student s1 = new Student(); Thread t1= new Thread(s1); Thread t2= new Thread(s1); t1.setName("1"); t2.setName("2"); t1.start(); t2.start(); }
-
我们来看结果:老师1与老师2交替执行
2.3、Callable
特点:可以获取到多线程运行的结果
- 创建一个类实现callable接口
- 重写call (是有返回值的。表示多线程运行的结果)
-
public class Student implements Callable<Integer> { @Override public Integer call() throws Exception { //求1-100的合 int sum=0; for(int i=0;i<100;i++){ sum+=i; } return sum; } }
-
- 创建类的对象(表示多线程要执行的任务)
- 创建Futureask的对象(作用:管理多线程运行的结果)
- 创建Thread类的对象,并启动(表示线程)
-
void threadtest3() throws ExecutionException, InterruptedException { // 创建类的对象(表示多线程要执行的任务) Student s1 = new Student(); // 创建Futureask的对象(作用管理多线程运行的结果) FutureTask<Integer> f1 = new FutureTask<Integer>(s1); // 创建Thread类的对象,并启动(表示线程) Thread t1 = new Thread(f1); t1.start(); // 获取线程执行结果 Integer i1 = f1.get(); System.out.println(i1); }
-
答案为:5050
看一下三种方法的优缺点
三、Thread中常用的成员方法
3.1、成员方法示例
3.1.1 get与set
public static void main(String[] args){
Demo1 demo1 = new Demo1();
Demo1 demo2 = new Demo1("设置了名字的线程2");
Demo1 demo3 = new Demo1();
demo3.setName("设置了名字的线程3");
demo1.start();
demo2.start();
demo3.start();
}
public class Demo1 extends Thread{
public Demo1(String name) {
super(name);
}
public Demo1() {
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
3.1.2 currentThread()
public static void main(String[] args){
Thread thread = Thread.currentThread();
System.out.println(thread.getName());//main
}
3.1.3 getPriority()与setPriority()
线程分为10档 最小为1 最大为10 默认就是5吗,优先级不是绝对的 他是一个概率问题。
public static void main(String[] args){
Thread thread = Thread.currentThread();
System.out.println(thread.getPriority());//5
}
public static void main(String[] args){
Thread thread = Thread.currentThread();
thread.setPriority(10);
System.out.println(thread.getPriority());//10
}
3.1.4 setDaemon()守护线程
守护线程就是:当非守护线程执行完毕之后,守护线程也会陆陆续续的停止,无论是否执行完毕,但是不会马上停止,会有个过程。
public class Demo1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class Demo2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public static void main(String[] args){
Demo1 demo1 = new Demo1();
Demo2 demo2 = new Demo2();
demo1.setName("非守护线程");
demo2.setName("守护线程");
demo2.setDaemon(true);
demo1.start();
demo2.start();
}
这里的Demo2对象被设定为守护线程,他原本要执行1w次的,现在执行结果。
非守护线程执行结束之后,他只执行到了20次就结束了。
四、线程的生命周期
五、线程安全问题与解决方案
5.1 线程安全问题的发生
现在有一个订单秒杀,总共是100份,分三个平台来卖,我们看一下代码:
public class Goods extends Thread {
// 加static就代表类对象共用一个count
static int count=0;
@Override
public void run() {
while (count<100){
count++;
System.out.println(getName()+"正在卖第"+count+"个商品");
}
}
}
public static void main(String[] args) {
Goods good1 = new Goods();
Goods good2 = new Goods();
Goods good3 = new Goods();
good1.setName("某宝");
good2.setName("某东");
good3.setName("某多多");
good1.start();
good2.start();
good3.start();
}
看结果:很明显,有问题,三家商城共卖一个?这能对吗,这肯定是不对的。这种问题就是线程并发的安全问题。
其包括有:
1. 不同商铺卖同一个商品问题2. 超卖问题,也就是只有100个商品却卖了103个的问题
5.2 同步代码块
把操作共享数据的代码锁起来:
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
我们接下来看看通过同步代码块修改之后的代码:
// 加static就代表类对象共用一个count
static int count=0;
// 锁对象 锁对象非常的随意 但是切记需要唯一
static Object object = new Object();
@Override
public void run() {
while (true){
synchronized (object) {
if(count<1000) {
count++;
System.out.println(getName() + "正在卖第" + count + "个商品");
}else {
break;
}
}
}
}
public static void main(String[] args) {
Goods good1 = new Goods();
Goods good2 = new Goods();
Goods good3 = new Goods();
good1.setName("某宝");
good2.setName("某东");
good3.setName("某多多");
good1.start();
good2.start();
good3.start();
}
既没有超卖问题,也没用同一个店铺卖同一个商品的问题
5.3 同步方法
就是把synchronized关键字加到方法上。
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定:
非静态:this
静态:当前类的字节码文件对象
public class Goods extends Thread {
// 加static就代表类对象共用一个count
static int count=0;
// 锁对象 锁对象非常的随意 但是切记需要唯一
static Object object = new Object();
@Override
public void run() {
while (true){
if (extracted()) {
break;
}
}
}
private synchronized boolean extracted() {
if(count==1000) {
return true;
}else {
count++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + count + "个商品");
}
return false;
}
}
public static void main(String[] args) {
Goods gd = new Goods();
Thread good1 = new Thread(gd);
Thread good2 = new Thread(gd);
Thread good3 = new Thread(gd);
good1.setName("某宝");
good2.setName("某东");
good3.setName("某多多");
good1.start();
good2.start();
good3.start();
}
5.4 Lock
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
1. void lock():获得锁
2. void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例。
注意:
在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。 java.concurrent.LockShouldWithTryFinallyRule.rule.desc
Positive example: Lock lock = new XxxLock(); // ... lock.lock(); try { doSomething(); doOthers(); } finally { lock.unlock(); }
Negative example: Lock lock = new XxxLock(); // ... try { // If an exception is thrown here, the finally block is executed directly doSomething(); // The finally block executes regardless of whether the lock is successful or not lock.lock(); doOthers(); } finally { lock.unlock(); }
来看例子:
这里要注意的点是,首先 static Lock lock = new ReentrantLock();锁要加上static作为唯一的锁,第二是释放锁要在finally代码块中。
public class Teacher {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.setName("王老师");
s2.setName("张老师");
s1.start();
s2.start();
}
}
// 第二个类
public class Student extends Thread {
static int studentID = 0;
static Object o1 = new Object();
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (studentID < 100) {
sleep(50);
studentID++;
System.out.println(getName() + "调用学号为:" + studentID + "的学生去干活");
} else {
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
检查过输出之后,无超过100,无重复调用
六、死锁
我们上个章节讲到了线程安全问题以及解决方案,大体的解决方案就是加锁,那么加锁虽然会解决多线程并发的安全问题,同时也会造成一个新的问题:死锁。
什么是死锁呢?打个比方:
两个人在一条窄窄的单行道上相向而行,每个人都在等待对方让路,以便自己能够通过。但因为两人都固执地站着不动,等待对方先让步,结果就是谁也无法前进,造成了一种僵持不下的局面。
在这个例子中,两个人就像是计算机中的两个进程,而这条单行道就像是计算机系统中的资源。如果每个进程都持有一个资源(比如一个人占据了道路的一部分),并且同时等待另一个进程释放它所持有的资源(另一个人占据的部分),而对方也在做同样的事情,那么双方就会陷入死锁状态,除非外部干预,否则他们都无法继续前进。
死锁并不是一个知识点,他是一个错误,需要我们在开发中避免。
那么死锁一般都是怎么发生的呢?
正常来讲一般都是锁里面套另外一个锁,会发生死锁现象 !
public class main {
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Locked lock1");
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock2) {
System.out.println("Thread 1: Locked lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Locked lock2");
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock1) {
System.out.println("Thread 2: Locked lock1");
}
}
});
t1.start();
t2.start();
}
}
}
在这个例子中,我们有两个线程
t1
和t2
,它们分别尝试以不同的顺序锁定两个资源resource1
和resource2
。每个线程首先锁定一个资源,然后稍作等待(模拟一些工作),接着尝试锁定另一个资源。
- 线程
t1
首先锁定resource1
,然后尝试锁定resource2
。- 线程
t2
首先锁定resource2
,然后尝试锁定resource1
。如果线程
t1
在线程t2
锁定resource2
之前锁定了resource1
,并且线程t2
在同一时间锁定了resource2
,那么它们都会等待对方释放资源,从而形成死锁。每个线程都持有一个资源并且等待另一个线程释放它需要的资源,但没有线程能够继续执行,因为所需的资源被对方持有。