0%

【Java基础】多线程

Java多线程学习,自己整理的学习笔记

一、基础概念

  • 程序:
    • 完成特定的任务,用某种语言编写的一组指令的集合。(静态代码)
  • 进程:
    • 运行起来的程序
  • 线程:
    • (1)进程的进一步细化,进程执行的其中一条路径
    • (2)线程拥有独立的栈和独立的程序计数器
    • (3)多个线程共享堆和方法区,方便线程之间的通讯,但也带来安全隐患

  • 并行和并发:
    • 并行:多个CPU同时执行多个任务
    • 并发:一个CPU“同时”执行多个任务

  • 多线程优点:
    • 提高应用程序的响应,对图形化界面更有意义,可以增强用于体验
    • 提高计算机系统CPU的利用率
    • 改善程序结构,将复杂的进程分为多个线程,利于理解和修改
  • 何时需要多线程:
    • (1)程序需要同时执行多个任务
    • (2)程序实现一些需要等待的任务。如用户输入、文件读写操作、网络操作、搜索等
    • (3)需要一些后台运行的程序

二、创建多线程

多线程的创建,需要使用到java.lang.Thread类中的内容

2.1 创建线程的方法

  • 方法一:
    • (1)创建一个继承与Thread的子类
    • (2)重写Thread类的run() -> 将线程执行的操作[方法体]声明在run()中
    • (3)常见Thread类子类的对象
    • (4)通过此对象调用start()来新开一个线程来执行,不是使用main()中的线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代码测试:
public class ThreadTest {
public static void main(String[] args) {

MyThread myThread = new MyThread();//创建对象
myThread.start();//调用对象start()方法

System.out.println("helloworld");

}
}

//例子:遍历100以内的偶数
class MyThread extends Thread{//继承Thread类
@Override
public void run() {//重写run方法
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
整合方法:
public class ThreadTest {
public static void main(String[] args) {

//可以通过匿名子类对象,上面几步方法整合
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}.start();

}
}

  • 方法二:
    • 1.创建一个实现了Runnable接口的类
    • 2.实现了类去实现Runable中的抽象方法:Run()
    • 3.创建实现类的对象
    • 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    • 5.通过Thread类的对象调用start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MThread implements Runnable{//1.创建一个实现了Runnable接口的类

@Override
public void run() {//2.实现了类去实现Runable中的抽象方法:Run()
for (int i = 0; i < 100; i++) {
if (i%2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}

}

public class ThreadCreate2 {
public static void main(String[] args) {

MThread mThread = new MThread();//3.创建实现类的对象

Thread thread = new Thread(mThread);//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

thread.start();//5.通过Thread类的对象调用start()
}
}
  • 比较创建线程的两种方式:
    • 1.开发中,优先选择:实现Runnable接口的方式
      • (1)实现的方式没有类的单继承性的局限性
      • (2)实现的方式更适合处理多个线程共享数据的情况
    • 2.联系:Thread类其实也是继承于Runnable接口
      • public class Thread implements Runnable
    • 3.相同点:两个方式都需要重新run(),将执行的逻辑声明在run()中

三、Thread中的方法

3.1 基本方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
start():启动当前线程,调用当前线程的run()

run():通常需要重写此方法,将创建的线程要执行的操作声明在此方法中

currentThread():静态方法,返回当前代码的线程

getName():获去当前线程名字

setName():设置当前线程的名字

yield():释放当前CPU的执行权

join():在线程A中调用线程B的join(),此时线程A进入阻塞状态,知道线程B执行完后,线程A才继续执行(可以理解为插队,但线程不一样)

stop():已过时,执行此方法时,强制结束当前线程

sleep():使当前线程“睡眠”一段时间[单位:毫秒],在该时间内线程是阻塞状态。静态方法,会报异常,需要try-catch

isAlive():判断线程是否还存活

3.2 线程优先级

  • 在Thread方法中存在三个常量,来表示线程的优先级
1
2
3
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5 --> 默认优先级
  • 通过get、set方法来获取设置优先级
1
2
getPriority(); 获取优先级
setPriority(); 设置优先级
  • 注意:优先级高只是被CPU执行的概率变高,但不一定是优先执行

四、线程的生命周期


五、同步

同步是用于解决使用共享数据引发的线程安全问题

5.1 经典线性安全问题

  • 100张票,3个窗口同时进行售卖
    (1)使用继承Thread类方式实现:
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
public class Thread1Test {
public static void main(String[] args) {
Window w1 = new Window();//创建三个对象,达到三个线程效果
Window w2 = new Window();
Window w3 = new Window();

w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");

w1.start();
w2.start();
w3.start();
}
}

class Window extends Thread{
private static int ticket = 100;//静态变量保证三个对象使用同一变量

@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(getName() + "卖票,票号为:" + ticket);
ticket --;
}
else {
break;
}
}
}
}

(2)使用Runnable接口的方式实现

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
public class Thread2Test {
public static void main(String[] args) {
Window1 window1 = new Window1();//创建实现类对象

Thread t1 = new Thread(window1);//通过实现类对象创建三个线程
Thread t2 = new Thread(window1);
Thread t3 = new Thread(window1);

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

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

class Window1 implements Runnable{//实现Runnable接口

private int ticket = 100;

@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖票:票号" + ticket);
ticket--;
}
}
}
}
  • 以上的方法都会出现重票,错票的情况,是由于使用了共享数据ticket的问题

5.2 同步方法一:同步代码块

1
2
3
synchronized(同步监视器){
//需要被同步的代码--->操作共享数据的代码
}
  • 同步监视器: 俗称“锁”,任何一个类的对象都可以充当锁,但必须多个线程用的是同一把锁
  • 补充: 可以考虑使用”this”或者”类名.class”来充当锁,但慎重考虑是否是唯一的锁

(1)对继承Thread方法的进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Window extends Thread{
private static int ticket = 100;//静态变量保证三个对象使用同一变量

@Override
public void run() {
while (true){

synchronized(Window.class){//使用“类名.class”,来充当锁,并且是唯一的
if (ticket > 0){
System.out.println(getName() + "卖票,票号为:" + ticket);
ticket --;
}
else {
break;
}
}

}
}
}

(2)对实现Runnable接口的进行修改

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
class SaleTicket implements Runnable{

private int ticket = 100;

Object obj = new Object();//任意一个类对象当同步监视器

@Override
public void run() {
while (true) {

synchronized (obj) {//同步代码块
//可以偷懒直接用synchronized(this),用当前对象“st”充当锁
//还可以synchronized(SaleTicket.class),类也算是对象,因为只会加载一次,是唯一对象
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票:票号" + ticket);
ticket--;
}
else {
break;
}
}

}
}
}

5.3 同步方法二:同步方法

1
2
3
private synchronized void 方法名(){
//操作共享数据的代码
}
  • 说明: 将操作共享数据的代码封装到一个方法中,并对该方法声明为synchronized
  • 补充:
    • 1.同步方法依然使用了同步监视器,只是不需要我们显示声明
    • 2.非静态同步方法的同步监视器是:this
    • 3.静态同步方法的同步监视器是:当前类本身

(1)对继承Thread方法的进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SaleTicket2 implements Runnable{

private static int ticket = 100;

@Override
public void run() {
while (true) {
show();
if (ticket == 0){
break;
}
}
}

//使用synchronized关键字修饰方法,变成同步方法
private static synchronized void show() {//静态方法的同步监视器为类的本身,静态才能使得同步监视器唯一
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票:票号" + ticket);
ticket--;
}
}
}

(2)对实现Runnable接口的进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SaleTicket2 implements Runnable{

private int ticket = 100;

@Override
public void run() {
while (true) {
show();
if (ticket == 0){
break;
}
}
}

//使用synchronized关键字修饰方法,变成同步方法
private synchronized void show() {//非静态方法,使用默认同步监视器:this
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票:票号" + ticket);
ticket--;
}
}
}

六、死锁

6.1 死锁的理解

  • 不同的线程分别占用了对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
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
//举例
public class DeadLockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();

//线程一
new Thread(){
@Override
public void run() {

synchronized(s1){//第一把锁
s1.append("123");
try {
Thread.sleep(100);//添加阻塞提高死锁概率
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (s2){//第二把锁
s2.append("456");
System.out.println(s1);
System.out.println(s2);
}

}
}
}.start();

//线程二
new Thread(new Runnable() {
@Override
public void run() {
synchronized(s2) {//与前面线程锁互换
s2.append("123");

synchronized (s1) {
s1.append("456");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
//线程一拿着s1锁,阻塞一段时间。线程二拿了s2锁,准备拿s1锁却被线程一拿了。线程一醒了,需要s2锁,但s2锁被线程二拿了没释放出来。
  • 说明:
    • (1)出现死锁后,不会出现异常,不会出现提示,只是所有线程进入阻塞状态,无法执行
    • (2)使用同步时,要注意避免死锁。就如使用循环避免死循环。

七、线程通信

7.1 理解

  • 线程通信是指线程之间交互执行

7.2 实现方法

  • wait(),使线程进入阻塞状态,并会释放当前拥有的锁
  • notify(),唤醒被wait()的一个线程,有多个被wait()的线程,唤醒优先级高的线程
  • notifyAll(),唤醒所有被wait()的线程

7.3 举例

  • 线程通信例子:使用两个线程打印1-100。线程1,线程2,交替打印
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
public class communicationTest {
public static void main(String[] args) {
Number number = new Number();

Thread t1 = new Thread(number);
Thread t2 = new Thread(number);

t1.setName("线程1");
t2.setName("线程2");

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

class Number implements Runnable{
private int num = 1;
@Override
public void run() {
while(true){
synchronized (this) {
this.notify();//使被wait()的线程进入就绪状态

if (num <= 100){
System.out.println(Thread.currentThread().getName() + "--打印了:" + num);
num ++;

try {
this.wait();//使得调用wait()方法的线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}

}else{
break;
}
}
}
}
}

八、JDK5.0后新增的内容

8.1新增同步方法

  • Lock锁:
    • 1.导入java.util.concurrent.locks.ReentrantLock包
    • 2.创建一个实例化的Reentrantlock对象
      • 可以选择传入参数“true”,使线程之间交互执行,形成线程通信
    • 3.try-finally包裹执行共享数据的代码
    • 4.在try{}内调用lock方法上锁,在finally{}调用unlock方法解锁
    • 注意:lock对象也需要每个线程使用的都是同一对象
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
51
public class LockTest {
public static void main(String[] args) {
Windows w = new Windows();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

t1.start();
t2.start();
t3.start();

}
}

class Windows implements Runnable{
private int ticket = 100;
//1.实例化Reentrantlock对象
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true){
try{
//2.调用对象的lock方法
lock.lock();

if (ticket > 0){

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + "卖票:票号为" + ticket);
ticket --;
}else{
break;
}
}finally{
//3.调用解锁方法unlock
lock.unlock();
}
}
}
}

8.2 新增线程创建方法

  • (1)实现Callable接口
    • 1.创建一个Callable的实现类
    • 2.实现call方法,将次线程需要执行的操作声明在call()中
    • 3.创建Callable的实现类对象
    • 4.将实现类对象作为参数,传递到FutureTask中,并创建FutureTask对象
    • 5.FutureTask对象作为参数,传递到Thread中,并创建出对象,并start方法调用
    • 6.若是需要返回值,可以通过FutureTask对象.get()来获取返回值,会抛出异常
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
  //1.创建Callable接口实现类
class NumThread implements Callable {
@Override
public Object call() throws Exception {//2.实现call()方法,将执行代码放在里面
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum; //测试Callable接口返回值
}
}

public class CreateMethod3 {
public static void main(String[] args) {
NumThread numthread = new NumThread();//3.创建Callable实现类的对象

FutureTask futureTask = new FutureTask(numthread);//4.将对Callable对象作为参数,创建Future对象

new Thread(futureTask).start();//将FutureTask对象作为参数,创建Thread对象,启动start()

try {
//get()返回值为FutureTask构造器参数callable实现类重新call的返回值
Object sum = futureTask.get();
System.out.println(sum);
} catch (Exception e) {
e.printStackTrace();
}
}
}
    • 如何理解实现Callable接口 比 Runnable接口强大
      • 1.call()可以有返回值
      • 2.call()可以抛出异常
      • 3.Callable是支持泛型

  • (2)线程池
    • 1.提供指定数量的线程池
    • 2.设置线程属性(可选)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后悔终止
    • 3执行指定线程的操作,需要提供实现Runnable接口 或 Callable接口的实现类的对象
      • Runnable 用 execute()
      • Callable 用 submit()
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
public class CreateMethod4 {
public static void main(String[] args) {
//1.提供指定数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);

//2.设置线程池属性
//ExecutorService是一个接口,需要用其实现类进行属性设置,可以进行强转;
((ThreadPoolExecutor) service).setMaximumPoolSize(10);

//3.执行指定线程的操作,需要提供实现Runnable接口或Callable接口的实现类的对象
service.execute(new RunTest());//适合使用于Runnable
service.submit(new CallTest());//适合使用于Callable

service.shutdown();//关闭连接池
}
}

//Runnable接口实现类
class RunTest implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i%2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}

//Callable接口实现类
class CallTest implements Callable {
@Override
public Object call() throws Exception {
for (int i = 1; i <= 100; i++) {
if (i%2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
return null;//一定要有return
}
}
    • 线程池的好处:
      • 1.提高响应速度(减少了创建新线程的时间)
      • 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
      • 3.便于线程的管理

九、面试题

  • (1)synchronized 与 lock的异同?

    • 1.相同:
      • 二者都可以解决线程安全问题
    • 2.不同点:
      • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
      • lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
  • (2)sleep() 与 wait()方法的区别

    • 1.相同点:
      • 都可以使得当前线程进入阻塞状态
    • 2.不同点:
      • 声明位置不同,sleep()是声明在Thread类中的,wait()是声明在Object()类中
      • 调用要求不同:sleep()可以在任何场景下调用。wait()只能在同步代码块或同步方法中
      • 如果都在同步代码块 或 同步方法 中调用,sleep()不会释放同步监视器,wait()方法会