【JavaEE】 多线程-初阶

多线程-初阶

1. 认识线程

1.1 概念

1) 线程是什么
  • 多个线程组成了一个进程,线程好比是一跟光纤中的一个玻璃丝,进程是整根光纤。

  • 一个进程中的线程共享这个进程中的资源(内存、硬盘)

2) 为什么需要线程

单核CPU发展出现瓶颈,想要再提高算力,只能增加CPU个数,并发编程就是利用多核CPU的绝佳方式.

使用进程也可以实现并发编程,只是进程重量大,创建销毁消耗资源多, 所以更好的方式是使用线程进行并发编程.

3) 线程和进程的区别
  • 线程包含于进程
  • 每个进程至少有一个线程, 即main线程(main thread)
  • 进程之间互不干扰, 但是线程之间耦合度高(一个线程出现问题, 其他线程也会崩溃)
  • 进程是系统分配资源的最小单位, 线程是系统调度的最小单位
4) Java中线程 和 操作系统线程 的关系

Java中线程是对于操作系统线程的封装和抽象.


1.2 第一个多线程程序

public class Main {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread1.");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t1 = new Thread(r1);

        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread2.");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }
            }
        };
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

运行结果:

hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread2.
hello thread1.
hello thread1.
hello thread2.

1.3 创建线程

1) 继承Thread
class MyThread extends Thread{
    public void run() {
        System.out.println("继承Thread得到");
    }
}
public class Demo1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

运行结果:

继承Thread得到
2) 实现Runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口得到");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyRunnable r1 = new MyRunnable();
        Thread t1 = new Thread(r1);

        t1.start();
    }
}

运行结果:

实现Runnable接口得到

继承Thread和实现Runnable接口的this指代的对象不同, 前者直接指代这个线程, 后者指代接口, 想要指代线程需要使用Thread.currentThread().

4) 使用匿名内部类
public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用Thread匿名内部类,直接传参new Runnable接口得到");
            }
        });

        Thread t2 = new Thread() {
            public void run() {
                System.out.println("使用Thread匿名内部类,直接重写run方法得到");
            }
        };
        
        t1.start();
        t2.start();
    }
}

运行结果:

使用Thread匿名内部类,直接传参new Runnable接口得到
使用Thread匿名内部类,直接重写run方法得到
5) 使用lamda表达式
public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            // 不需要重写run,lamda表达式就相当于是run方法
            System.out.println("lamda表达式创建得到");
        });

        t.start();
    }
}

运行结果:

lamda表达式创建得到

1.4 使用多线程编程可以增加程序的运行速度

但是可能导致程序线程不安全, 需要合理加锁.


2. Thread类常见方法

2.1 Thread常见构造方法

构造方法名 说明
Thread() 普通构造(仅分配空间)
Thread(Runnable) 根据所给的run()构造对象
Thread(String) 为将构造出的线程进行命名
Thread(Runnable, String) 根据run()创建对象并命名

命名主要是为了方便调试.


2.2 Thread常见属性

方法名 作用
start() 创建线程并运行
getId() 返回线程的Id
(这个Id不同于操作系统未进程分配的Id,也不是PCB中的Id,仅仅是JVM分配的Id)
getName() 返回线程名字
getPriority() 返回优先级
getState() 返回线程目前的状态(NEW, RUNNABLE, WAITING, TIMED_WAITING,BLOCKED,TERMINATED)
isDaemon() 判断是否为后台进程(后台进程不决定一个线程的存亡,只有前台进程才决定)
isAlive() 判断是否存活
isInterrupted() 判断是否被中断

2.3 让一个Thread跑起来

使用start()方法即可使其开始运行.

之前写过的run方法, 只是为这个线程规定要怎么做, 只有start方法才能启动线程.

2.3.1 start 和 run 的区别

start会调用系统api进行创建线程

run只是一个普通的方法,告诉线程的执行逻辑,不会创建线程

2.4 中断一个线程

有两种方式:

  • 设置一个记号, 线程A线程B共享这个记号, 两个线程约定一个在其为true时工作, 一个在其为false时工作, 此时如果在A中对于这个记号进行更改, 那就能够使得B停止工作.
public class Demo5 {
    // 设置共同变量
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("t1");
            // 在第一个线程执行完后暂停3秒
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 3秒后,设置共同变量为false
            flag = false;
        });

        Thread t2 = new Thread(()->{
            while (flag == true) {
                System.out.println("t2");
            }
        });

        t1.start();
        // 在第二个线程执行前暂停2秒,让t1线程运行2秒
        Thread.sleep(2000);
        // 意味着t2只能执行1秒
        t2.start();
    }
}

运行结果:

t1
(等待3)
t2
t2
...
t2
t2

在3秒后,t1线程将共享变量修改为false, 所以t2被中断.


  • 调用interrupt()进行通知
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (!Thread.interrupted()) {
                System.out.println("t1尚未被中断");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("t1收到中断信号");
                    throw new RuntimeException(e);
                }
                break;
            }
        });

        System.out.println(t1.getState());
        t1.start();
        System.out.println(t1.getState());
        // 暂停2秒后进行中断
        Thread.sleep(1);
        t1.interrupt();
    }
}

运行结果:

NEW
RUNNABLE
t1尚未被中断
t1收到中断信号
Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
	at Demo6.lambda$main$0(Demo6.java:10)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at Demo6.lambda$main$0(Demo6.java:7)
	... 1 more

interrupted() 和 currentThread().isInterrupted() 截然不同:

方法名 说明
interrupted() 查看当前线程是否被中断, 清除标记为false
currentThread().isInterrupted() 查看当前线程是否中断,仅作判断, 不清除标记

演示:

public class Demo7 {
     public static void main(String[] args) {
     Thread t1 = new Thread(()->{
         for (int i = 0; i < 10; i++) {
             //System.out.println(Thread.interrupted());
             System.out.println(Thread.currentThread().isInterrupted());
         }
     });
     t1.start();
     t1.interrupt();
 }
}

运行结果:
true
true
true
true
true
true
true
true
true
true
//(这种方法不清除中断标记, 仅作判断)
public class Demo7 {
     public static void main(String[] args) {
     Thread t1 = new Thread(()->{
         for (int i = 0; i < 10; i++) {
             System.out.println(Thread.interrupted());
         }
     });
     t1.start();
     t1.interrupt();
 }
}

运行结果:
true
false
false
false
false
false
false
false
false
//(这种方法清除中断标记, 恢复为未被中断状态)

2.5 等待一个线程

线程执行有先后顺序的时候**(线程A的执行需要依赖于线程B的执行结果), **那就需要使用join()方法, 这个方法能够保护当前的线程执行完毕后,其他线程才会去执行.

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static int count = 0;// 1

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            System.out.println("t11");
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t11");
        });

        Thread t2 = new Thread(()-> {
            System.out.println("t21");
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            System.out.println("t21");
        });

        t1.start();
        t1.join();

        t2.start();
        Thread.sleep(100);

        System.out.println(count);
    }
}

在都对count进行++五万次的操作中,可以不加锁,也能使得count得到预期值的方法就是让t2在t1执行结束之后才启动,这样两个线程都能完成自己的任务,得到预期count。

2.5.1 方法中不能够加public、static等修饰词

访问局部变量的过程:对象-> 方法->局部变量。访问局部变量就已经有了访问权限的设定了。由此加修饰符也成了摆设。
对应static来说,因为static只能修饰成员变量和成员方法,在局部变量中用static修饰,又不能直接被类调用。

2.6 获取当前线程的引用

使用 Thread.currentThread();进行获取。

2.7 休眠线程

使用 Thread.sleep(long mills)实现。


在线程内部需要捕获异常,在方法中使用需要抛出异常。

Thread t2 = new Thread(()-> {
    System.out.println("t21");
    for (int i = 0; i < 50000; i++) {
        count++;
    }
    // 捕获异常
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}, "t2");
// 抛出异常
public static void main(String[] args) throws InterruptedException {

3. 线程的状态

3.1 线程的所有状态

NEW: 安排了工作, 还未开始行动

RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.

BLOCKED: 这几个都表示排队等着其他事情

WAITING: 这几个都表示排队等着其他事情

TIMED_WAITING: 这几个都表示排队等着其他事情

TERMINATED: 工作完成了

使用isAlive()方法可以观察线程是否存活。

使用yield()方法会使线程重新排队。

4. 线程不安全

4.1 线程不安全发生的时机

在两个线程进行对于同一个变量进行修改的时候会出现线程不安全的问题。

4.2 线程不安全发生的原因

  1. 指令非原子性

    即使是“++”这个操作,仅有一条语句,也是由3条微指令构成的:

    1. 从内存中读出count到寄存器

    2. 在寄存器中完成++

    3. ++后的值放进寄存器。

    在多个线程的这三个操作如果相互穿插进行,那么就可能会读入”脏值“。

  2. 内存可见性

    内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.

    对于多次重复的读入同一个数据,编译器会对其进行优化,直接在寄存器中使用这份数据的拷贝值,不再从内存中进行读取,对这个变量的修改操作也都是在这个拷贝值身上完成,在这个线程使用完此变量后才会将最终值写进内存。

    这种方式对于单线程来说是一种优化,简便了数据的读取操作,但是对于多线程来说,如果在线程A频繁修改变量count的同时,

线程B需要对count进行修改,那么就会读到“脏值”。

4.2.1 解决内存可见性问题

使用volatile关键字,忽略编译器对其的优化。

5. synchronized 关键字——解决线程不安全问题

synchronized 会将其所在的代码块进行加锁。


5.1 synchronized 特性

1) 互斥

如果说一个代码块相当于是一间房,那么一个synchronized就相当于是给这个房间进行上锁,其他人想进去必须要等到里面的人把锁打开,两个人进行争夺房间的使用权的过程也称为“锁竞争”。

锁的作用就是让不同的线程拥有同一个对象的锁的时候,只有执行顺序靠前的线程能够正常运行,后面的线程需要等待前面的线程释放锁以后才能继续正常运行。

2)刷新内存
底层实现:

synchronized的底层是使用操作系统的mutex lock实现的.

synchronized工作过程本质上是通过获取一个安全的空间来进行保证操作原子性的:

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存 3.

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

3) 可重入性
public static final Object locker = new Object();

synchronized (locker) {
    synchronized (locker) {

    }
}

在对于一个对象上同一把锁两次的时候,理论上来说会产生“死锁”现象。

因为一个第二把锁所在的代码块执行的前提是第一把锁释放,但是第一把锁释放的条件是后序的代码块执行完,形成闭环,造成“死锁”。

死锁的成因

1)互斥使用:同一把锁的不同线程同一时间只有一个能够运行

2)不可抢占:后面的线程只能等前面的将锁释放后才能运行

3)循环等待:在A阻塞等待B释放锁的时候,B在等待A释放锁

4)请求保持:一个线程尝试获取多把锁(线程A在已经被锁1加上的情况下获取一个已经被占用的锁2,那么锁1不会被释放)

1和2都是锁的基本特性,3和4是代码结构,当同时满足以上四点的时候才会发生死锁。

5.2 synchronized 使用示例

1)给普通方法上锁

synchronized public void method1() {

}

2)给静态方法上锁

synchronized public static void method1() {

}

3)给代码块上锁

给当前对象上锁
//3
public void method2() {
    synchronized (this) {

    }
}
给类对象上锁
//4
public void method3() {
    synchronized (Demo2.class) {

    }
}

其中,3和4等价。

5.3 Java 标准库中的线程安全类

不安全的:ArrayList 、LinkedList、 HashMap、 TreeMap、 HashSet、 TreeSet、 StringBuilder

安全的:Vector (不推荐使用)、 HashTable (不推荐使用) 、ConcurrentHashMap、 StringBuffer

6. volatile关键字

6.1 volatile能够保证内存可见性

内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.

import java.util.Scanner;

// volatile的作用
public class Demo3 {
    //public volatile static int isQuit = 0;
    public static int isQuit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            while (isQuit == 0) {

            }
            System.out.println("t1退出");
        });

        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(()-> {
            System.out.println("请输入");
            Scanner scanner = new Scanner(System.in);
            isQuit = scanner.nextInt();
        });
        t2.start();
        Thread.sleep(1000);

        System.out.println(t1.getState());
    }
}

由于编译器的优化, t2对于isQuit变量进行修改并不影响t1线程中看到的isQuit变量是0, 这就叫做内存不可见.

但是如果加上volatile, 那么编译器会保证内存的可见性, 放弃优化.(所以会将代码的运行效率降低)

volatile的工作过程:

  1. 将内存中的数据放进寄存器
  2. 线程对于数据进行修改
  3. 将数据写回内存

如果是读取:

  1. 读取最新值进入工作内存
  2. 从工作内存中读取volatile变量的副本

6.2 volatile不能保证操作的原子性

volatile虽然一次性将数据读取到工作内存, 待其写完后又放回主内存, 但是在写的过程中, 如果其他线程也对同一个变量进行写入, 这将是合法的, 并且存在线程安全问题.

// 线程安全问题
public class Main {
    // 加上volatile并不能够得到预期的count值
    public static volatile int count = 0;// 1

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        }, "t2");

        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(count);

    }
}

结果:

75377

6.3 但是synchronized可以保证内存可见性和原子性

你可能感兴趣的:(javaEE,java-ee,java,jvm)