Java 多线程基础总结

Java 多线程基础总结

本文是Java多线程的基础入门篇

一、程序 进程 线程

  • 程序:指令集,静态概念
  • 进程:操作系统调度程序,动态概念
  • 线程:在进程内多条执行路径

线程是进程中包含的一个或多个执行单元。
线程只能归属一个进程。并且线程只能访问该进程所拥有的资源。
当操作系统创建一个进程,该进程会自动申请一个主线程作为首要执行的任务。
线程之间是独立的 一个进程内可以有多条并行的线程
一个线程是进程的一个执行单元
同类的多个线程共享一块内存空间和一组资源。
线程本身是一个有可供程序执行的堆栈。
线程的切换耗时小,把线程称为轻负荷进程。

进程和线程的关系:
1.一个进程至少要有一个线程。
2.线程的划分尺度一定是小于进程。
3.多个进程在执行过程中拥有独立的内存单元。
4.而多个线程共享内存。
5.线程在执行过程中与进程的区别在于每个独立的线程都有一个程序的执行入口,顺序执行,并有一个程序的执行出口。
6.从逻辑角度讲,多线程的意义在于一个应用程序中,有多个执行部分可以并发运行,但操作系统并没有将多个线程看作独立的应用来实现进程的调度和管理以及资源分配。

线程和进程的区别
根本区别:线程作为资源分配的单位
进程是调度和执行的单位
线程是进程的一部分 线程是不同的执行路径

并发:多个线程”同时”进行(),实际上多个线程是并发运行的。
操作系统将运行时的多个线程以时间片段划分,在不同的时间片段中随机切换。
Java支持多线程,即在Java应用程序中,可以创建多个线程,多个线程可以并发运行。

二、线程创建之一 继承Thread +run(线程体)

启动:创建子类对象 +对象.start()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 继承Thread类,定义线程类
* @author gavino
*/
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();//创建线程对象
t1.start();//启动线程
MyThread t2 = new MyThread();
t2.start();
}
}

继承Thread类方式的缺点

三、静态代理模式

* 1、真实角色
* 2、代理角色: 要持有真实角色的引用
* 3、二者要实现相同的接口

四、线程创建之二 类实现Runnable接口 +重写run(线程体)方法 –>真实角色类

启动:
1、创建真实角色
2、创建代理角色(Thread类) +真实角色引用
3、代理对象.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
/**
* 实现 Runnable 接口创建线程 描述的是线程任务,而不是线程
* 所以不能直接启动
* 注意 此时,MyThread 不是线程类,只是实现了接口的普通类
* @author gavino
*/
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
/**
* Runnable 接口,是线程任务的规范接口,实现Runnable接口的类,描述的是线程任务
* 可以使用一个Thread对象封装一个实现Runnable接口对象,进而实现多线程。
* @author gavino
*/
public class Test {
public static void main(String[] args) {
Runnable r1 = new MyThread();//创建一个任务对象
Thread t1 = new Thread(r1);//创建一个线程对象
t1.start();//调用的是Thread 的run()方法,由该run()方法调用r1的run()
Thread t2 = new Thread(r1);//创建第二个线程对象
t2.start();
}
}

匿名内部类创建并启动线程

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("匿名内部类多线程"+i);
}
}
}).start();
}

两种线程的区别

  1. 继承类,不能实现多个线程共享同一个实例资源
    实现接口,任务模块化,多个线程可以共享同一个资源
  2. 继承类,就不能继承其它类,有单继承局限性
    实现接口,还可以继承其他的父类

判断线程的个数,就看new 了多少个 Thread 或多少个 Thread 的子类。实现 Runnable的类并不是线程类

推荐使用Runnable创建线程
1)避免单继承的局限性
2)便于共享资源

五、线程创建之三 类实现Callable接口

run方法的缺点:
1、不能抛异常
2、没有返回值

Callable创建线程:优点–可以获取返回值,缺点–比较繁琐

1)创建Callable实现类 +重写call()方法
2)借助执行调度服务ExecutorService获取Future对象
ExecutorService ser = Executors.newFixedThreadPool(线程数量);
Future result = ser.submit(实现类对象);
3)获取值 result.get();
4)停止服务 ser.shutdown();

六、线程池

就是把若干用户线程添加到线程池中,由线程来统一管理线程

为什么要使用线程池:

  1. 减少了创建和销毁线程的次数,每个工作线程
  2. 可以根据系统的承受能力,调整线程池中
    结论:

线程池的使用:

有一个Executors 的线程工具类,此类提供了若干静态方法
这些静态方法
1.
线程池保证所有的任务是按照任务的提交顺序来执行

  1. Executors.newFixedThreadPool();
    创建固定大小的线程池,每次提交一个任务就创建一个

  2. Executors.newCachedThreadPool();
    创建了一个可以缓冲的线程池,如果线程大小超过处理任务所需要的线程,那么就回收部分线程,

  3. 创建一个大小无限制的线程池,此线程池支持定时以及周期性的执行任务的需求。

线程池
核心线程满了,线程会进入阻塞队列,阻塞式队列满了,才开临时线程
能执行的线程总数为: 核心线程数量 + 阻塞队列中的线程数量 + 临时线程数量

七、线程状态与停止线程、线程阻塞

(一)线程状态

  1. 新建状态(new): 创建一个线程对象
  2. 就绪状态(Runnable): 线程对象创建以后,调用start()方法,就绪状态的线程只处于等待cpu的使用权。变为可运行。
  3. 运行状态(Running): 就绪状态的线程,获取到了cpu资源,执行程序代码。
  4. 阻塞状态(Blocked):
    • 等待阻塞 调用wait()方法 没有占用cpu资源,也没有处于排队等待cpu状态
    • 睡眠阻塞 调用sleep()方法 占用cpu资源,也没有处于排队等待cpu状态
    • IO阻塞 进入IO操作,线程即进入到IO阻塞状态,当IO操作结束,即结束IO阻塞状态
  5. 死亡状态(Dead): 线程任务执行结束 即run()结束,该线程对象就会垃圾回收

(二)停止线程

  1. 自然终止:线程体正常执行完毕。
  2. 外部干涉:
    • 线程类中定义线程体使用的标识
    • 线程体使用该标识
    • 提供对外的方法改变该标识
    • 外部根据条件调用该方法即可

(三)线程阻塞

  1. join:合并线程(调用 join() 后,线程会等待 join() 所属线程运行结束后再继续运行。)
  2. yield:(屈服,让步)暂停自己的线程(从运行状态切换到就绪状态)
    就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行

注意是让自己或者其他线程运行,并不是单纯的让给其他线程。

  1. sleep:休眠(暂停当前线程) 不释放锁 常用于两种形式
    • 与时间相关的 如倒计时
    • 模拟网络延时

八、线程基本信息-优先级

优先级不代表执行的先后顺序 代表概率
MAX_PRIORITY 10
NORM_PRIORITY 5(默认)
MIN_PRIORITY 1
*setPriority() 设置优先级

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
/**
* 测试线程的优先级
* @author gavino
*/
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(i+" 线程:"+Thread.currentThread().getName()+" 优先级:"+Thread.currentThread().getPriority());
}
MyRun r1 = new MyRun();
MyRun r2 = new MyRun();
Thread t1 = new Thread(r1);
t1.setName("t1");
Thread t2 = new Thread(r2);
t2.setName("t2");
//获取线程的优先级
// System.out.println(t1.getPriority());
// System.out.println(t2.getPriority());
//设置线程优先级
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
class MyRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i+" 线程:"+Thread.currentThread().getName()+" 优先级:"+Thread.currentThread().getPriority());
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
0 线程:main 优先级:5
1 线程:main 优先级:5
2 线程:main 优先级:5
...
0 线程:t1 优先级:10
1 线程:t1 优先级:10
2 线程:t1 优先级:10
0 线程:t2 优先级:1
3 线程:t1 优先级:10
...
1 线程:t2 优先级:1
2 线程:t2 优先级:1
...

线程类中常用的API

  • currentThread():当前线程
  • getName():设置名称
  • setName(): 获取名称
  • isAlive():判断状态
  • setDaemon(): 设置一个线程为守护(后台)线程

九、线程同步与锁定

可能出现线程安全问题的条件

  1. 多个线程访问统一资源
  2. 多行代码对同一个变量进行运算

同步:并发 多个线程访问同一份资源 确保资源安全 –>线程安全
(常说 Hashtable是线程安全的 HashMap是线程不安全的)
synchronized–>同步
异步:

synchronized可以修饰一段代码或方法
(一)、同步块
synchronized(引用类型|this|类.class) {
}
注意:(重要)

  1. 给this对象加锁,即谁调用该方法,谁就是该this对象
  2. 串行还是并行,得看所调用的资源是否互斥
  3. 要使资源互斥,即加锁的对象应该为同一对象
  4. synchronized 作用在类上,类锁
    • synchronized static 和 synchronized(类名.class)
      此时锁在同一个类上

(两个方法)当锁分别加在对象上和类上时,但是加锁的目标不同,即锁定的资源不互斥,他们将并行运行
总结:

  • synchronized 关键字加的锁实际是加到对象上的,关键是找到加锁对象
  • synchronized 修饰,两个方法是并行还是串行,看锁定的资源是否互斥

(二)、同步方法
synchronized 修饰方法,并不是给方法加锁,而是给方法的调用对象加锁,JVM每次调用方法,都会处理锁(线程安全的方法效率低)
public synchronized void method(){…}
-——————————————————-
* 实例方法,this为默认的锁对象
* 类方法,当前类的字节码对象作为锁
* 同步代码段锁,可以是任意对象锁

单例设计模式:确保一个类只有一个对象(该对象为内部自己创建,外部只能调用这一个对象)
#【确保一个类只有一个对象】

懒汉式:(前面懒得创建对象,在使用的时候再创建对象)
1、构造器私有化 避免外部直接创建对象
2、声明一个私有的静态变量(属性)
3、创建一个对外的公共的静态方法 访问该变量,如果变量没有对象 创建该对象

饿汉式:
1、构造器私有化 避免外部直接创建对象
2、声明一个私有的静态变量(属性),同时创建对象
3、创建一个对外的公共的静态方法 访问该变量

十、死锁

过多的同步容易造成死锁

1
2
3
4
5
6
7
8
9
10
11
12
synchronized(o1){
......
synchronized(02){
......
}
}
synchronized(02){
......
synchronized(01){
.....
}
}

模拟线程死锁现象

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
/**
* @author gavino 模拟线程死锁现象
* 不同的线程分别占用对方需要的同步资源
* 都在等待对方放弃自己需要的同步资源
*
* 出现死锁后,不会出现异常
*/
public class Demo {
public static void main(String[] args) {
MyLock myLock = new MyLock();
YouLock youLock = new YouLock();
StringBuilder sb = new StringBuilder();

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (myLock) {
sb.append("my lock");
sb.append("hehehe");
System.out.println(sb);
synchronized (youLock) {
sb.append("your lock");
sb.append("hahaha");
System.out.println(sb);
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (youLock) {
sb.append("your lock");
sb.append("hahaha");
System.out.println(sb);
synchronized (myLock) {
sb.append("my lock");
sb.append("hehehe");
System.out.println(sb);
}
}
}
});
t1.start();
t2.start();
}
}
class MyLock {
}
class YouLock {
}

解决方法:生产者消费者模式(此处不指设计模式)
即:(过马路)信号灯法(标志位)
信号灯:flag
思路:
flag–>T 生产者生产 消费者等待 生产完成后通知消费
flag–>F 消费者消费 生产者等待 消费完成后通知生产

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
52
53
/*
* 一个场景 共同的资源
*/
public class Product {
//信号灯
private boolean flag = true;
private String pro;
//生产功能
public synchronized void produce(String pro) {
if (!flag) {
try {
this.wait();//
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始生产
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产了"+pro);
//生产完毕
this.pro = pro ;
//通知消费
this.notify();
//停止生产
this.flag = false;
}
//消费功能
public synchronized void consume() {
if (flag) {
try {
this.wait();//
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始消费
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费完毕
System.out.println("消费了"+pro);
//通知生产
this.notifyAll();
//停止消费
this.flag = true;
}
}

定义生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 生产者
*/
public class Producer implements Runnable {
private Product p;
Producer(Product p) {
this.p = p;
}
@Override
public void run() {
for(int i=0;i<20;i++) {
if(0==i%2) {
p.produce("产品A");
}else {
p.produce("产品B");
}
}
}
}

定义消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 消费者
*/
public class Consumer implements Runnable {
private Product p;
Consumer(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
p.consume();
}
}
}

应用

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//公共资源 产品
Product product = new Product();
//多线程 不同的对象访问同一份资源
Producer p = new Producer(product);
Consumer c = new Consumer(product);
new Thread(p).start();
new Thread(c).start();
}

注意
wait():等待 会释放锁
sleep()不释放锁
notify()/notifyAll():唤醒
wait() notify()/notifyAll()和synchronized一同使用
-—————————————————
wait和notify是object的方法,也就是说所有对象都有这两个方法。
notify() 唤醒处于等待阻塞状态的线程
wait() 将当前线程对象切换到等待阻塞状态
这两个方法可以用来阻塞当前线程(同时放弃互斥锁)或者是
唤醒其他调用wait方法陷入阻塞的线程(不能唤醒那些因为抢占锁而阻塞的队列)。
能够执行wait和notify的前提是代码已经进入了synchronized包含的代码块中。

十一、任务调度

Timer
schedule()
quartz 框架

十二、总结

重点:
* 创建线程
* 终止线程
* sleep
* 同步(面试中)

面试点
一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
也就是说 synchronized 获得的锁是可重入的。
子类也可以调用父类的同步方法

程序在执行过程中,如果出现异常,默认情况锁会被释放。(因此要非常小心的处理同步业务逻辑的异常)

拓展

JUC:

在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,
用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中
的 Collection 实现等。
quartz 框架:
Quartz是一个完全由java编写的开源作业调度框架。

高并发编程工具JUC

# Java

评论