多线程

概念

程序:未完成任务,用某种语言编写的一组指令的集合。

进程:程序的一次执行过程,或是正在内存中运行的应用程序。

线程:进程可以进一步细化为线程,是程序内部的一条执行路径。线程是CPU调度和执行的最小单位。

  • 不同进程之间是不共享内存的。

  • 进程之间的数据交换和通信的成本很高。

线程调度策略
分时调度:所有线程轮流使用CPU的使用权。并且平均分配每个线程占用CPU的时间
抢占式调度:让优先级高的线程以较大的概率优先使用CPU。如果线程的优先级相同,那么就会随机选一个。Java使用的是抢占式调度。

了解并行和并发

多线程的优点

提供应用程序的响应。

提高计算机CPU的利用率。

改善程序结构。将既长又复杂的进程拆为多个线程,独立运行,便于理解和修改。

多线程创建方式

方式一:继承Thread类的方式

  1. 创建一个继承于Thread类的子类

  2. 重写Thread类的run()

  3. 创建当前Thread的子类对象

  4. 通过对象调用start()

创建线程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ThreadTest {
public static void main(String[] args) {
//方式1:创建子类
MyThread myThread = new MyThread();
myThread.start();
MyThread2 myThread2 = new MyThread2();
myThread2.start();
//方式2:创建匿名子类
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}

方式二:实现Runnable接口

  1. 创建一个Runnable接口的实现类
  2. 实现接口中的run()
  3. 创建当前实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
  5. 实例调用start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Way2test {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread thread = new Thread(threadTest);//多态
thread.start();
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
Thread thread1 = new Thread(threadTest);
thread1.start();
//使用实现Runnable接口的方式创建线程,匿名实现类的匿名对象
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}).start();
}
}
class ThreadTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
}

对比一下两种方式:

共同点:①启动线程都是Thread类的start()方法②创建的线程对象都是Thread类或其子类的实例

不同点:前者是类的继承,后者是接口的实现

建议使用runnable接口的方式。

好处:①实现的方式,避免了单继承的局限性②更适合处理有共享数据的问题③实现了代码和数据的分离

联系:代理模式

方式三:实现Callable方式

方式四:使用线程池

方式三、四在此省略

Thread类

常用方法

线程中的构造器(记得子类不会继承父类的构造器)

Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)

线程中的常用方法

start():①启动线程②调用线程的run()
run():声明线程要执行的代码
currentThread():获取当前的线程
getName():获取线程名
setName():设置线程名
sleep(long mills):静态方法,调用可以是当前线程睡眠指定的毫秒数
yield():静态方法一旦执行此方法,就释放CPU的执行权
join():在线程a中通过线程b调用join方法,意味着线程a进入阻塞状态,直到线程b结束,线程a才会继续执行
isAlive():判断当前线程是否还存活

线程优先级

getPriority():获取线程的优先级
setPriority():设置线程的优先级(1~10)

线程的生命周期

jdk5之前

image-20250430202641386

jdk5之后对阻塞进行了细分,在此不做赘述,在JUC中会详细学习

线程安全问题

什么叫线程安全问题?

如何解决?

方式一:同步代码块解决安全问题

1
2
3
4
5
synchronized(同步监视器){

需要被同步的代码;

}

说明:

  • 需要被同步的代码,即操作共享数据的代码。
  • 共享数据:即多个线程都需要操作的数据。
  • 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
  • 同步监视器,俗称锁,哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
  • 同步监视器,可以用任何一个类的对象充当。但是,多个线程必须公用一个同步监视器。

在实现Runnable接口的方式中,同步监视器可以考虑使用this
在继承Thread类的方式中,同步监视器慎用this

方式二:同步方法解决安全问题

如果操作共享数据的代码完整声明在了一个方法中,那么我们就可以将此方法声明为同步方法。

非静态的同步方法,默认监视器是this;静态的同步方法,默认监视器是当前类本身(.class)。

synchronized的好处:解决了线程安全问题。

弊端:在操共享数据时,多线程其实是串行执行的,意味着性能低。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ThreadTest {
public static void main(String[] args) {
Account acc=new Account();
Customer customer1 = new Customer(acc,"甲");
Customer customer2 = new Customer(acc,"乙");
customer1.start();
customer2.start();
}
}
class Account{
private double balance;
public synchronized void deposit(int amount){
if(amount > 0){
balance+=amount;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存款成功,余额为:"+balance);
}
}
class Customer extends Thread{
Account account;
public Customer(Account account){
this.account = account;
}
public Customer(Account account,String name){
super(name);
this.account = account;
}
@Override
public void run(){
for(int i=0;i<3;i++){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.deposit(1000);
}
}
}

懒汉式单例模式

publicpublic class LazyOne {
    private static LazyOne instance;

    private LazyOne(){}

//方式1:线程不安全
public static synchronized LazyOne getInstance1(){
    if(instance == null){
        instance = new LazyOne();
    }
    return instance;
}
//方式2:线程安全
public static LazyOne getInstance2(){
    synchronized(LazyOne.class) {
        if (instance == null) {
            instance = new LazyOne();
        }
        return instance;
    }
}
//方式3:相比2效率提高
public static LazyOne getInstance3(){
    if(instance == null){
        synchronized (LazyOne.class) {
            try {
                Thread.sleep(10);//加这个代码,暴露问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(instance == null){
                instance = new LazyOne();
            }
        }
    }

    return instance;
}
/*
从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要   
volatile关键字,避免指令重排。
*/

形式二:使用内部类

1
2
3
4
5
6
7
8
9
10
11
12
public class LazySingle {
private LazySingle(){}

public static LazySingle getInstance(){
return Inner.INSTANCE;
}

private static class Inner{
static final LazySingle INSTANCE = new LazySingle();
}

}

内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。

此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。

此时的内部类,使用enum进行定义,也是可以的。

死锁

不同线程分别占用着对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

方式三:Lock锁的方式解决线程安全问题

Lock的使用

  1. 创建Lock实例,需要确保多个线程共用一个Lock实例。需要考虑将此对象声明为static final
  2. 执行lock()方法吗,锁定对共享资源的调用
  3. 执行unlock(),释放对共享资源的锁定

对比synchronized和Lock:
synchronized不管是同步代码块还是同步方法,都需要在一对{}之后,释放对同步资源的调用

Lock是通过两个方法控制需要被同步的代码,更灵活。Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高。

线程通信

涉及到三个方法的使用:

wait():线程一旦执行此方法,就进入等待状态。同时会释放对同步监视器的调用。

notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。如果多个被wait的线程优先级相同,那么就随机唤醒一个。被唤醒的线程从当初被wait的位置继续执行。

notifyAll():唤醒所有被wait的线程。

注意:

  • 这三个方法必须在同步代码块或同步方法中使用;
  • 这三个方法的调用者必须是同步监视器。否则会报IlleagalMonitorStateException异常
  • 这三个方法声明在Object类中

wait()和sleep()区别?

相同点:调用后都会使线程进入阻塞状态

不同点:

  • 声明的位置:

    • wait():声明在Object类中
    • sleep():声明在Thread类中,静态的
  • 使用的场景不同:

    • wait():只能使用在同步代码块或同步方法中
    • sleep():可以在任何需要使用的场景
  • 使用在同步代码块或同步方法中:

    • wait():一旦执行,会释放同步监视器
    • sleep():一旦执行,不会释放同步监视器
  • 结束阻塞的方式:

    • wait():到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
    • sleep():到达指定时间自动结束阻塞