并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。

  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

注意辨析宏观与微观的理解

<img src="多线程笔记.assets/image-20210605152309555.png" alt="image-20210605152309555" style="zoom:50%;" />

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每 一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分 时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行, 即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个 线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

  • 进程:是指一个内存中运行的应用程序(比如打开一个软件 至少占用1个进程),每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
线程:CPU 可执行的最小任务单元 

八核CPU - 八核16线程
单核 单线程 CPU

线程 让程序节省时间,提高效率
多线程的CPU 可以看作可以并行运算的机器

单线程 - - 交替运行 不能节省时间提高效率
-- 同时运行的效果
CPU 运算速度特别快
在两个线程之间实现快速切换执行

进程:线程的老大 -- 程序运行

线程调度:

分时调度

所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度

  • 设置线程的优先级

  • 抢占式调度详解

    大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们一边使用编辑器,一边使用腾讯会议软件,同时还开着qq、微信、IDEA等软件。此时,这些程序是 在同时运行,”感觉这些软件好像在同一时刻运行着“

    实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高

Java实现线程的三个方法:

Thread 类

1
2
3
4
1、创建一个类 继承 Thread 
2、重写run方法
3、此类创建对象调用 start()方法
每个线程对象都只能启动一次

Runnable接口

1、创建一个类  implements Runnable接口 
2、重写run方法
3、此类创建对象 交给Thread类对象调用 start()方法
4、此类创建对象 交给线程池 调用 start()方法
    每个线程对象都可以被Thread类对象重复启动     
    更轻量 方便 

Callable接口

1
2
3
4
1、	创建一个类  implements Callable接口 
2、重写call方法
3、此类创建对象 交给线程池 调用 start()方法
可以带有返回值

三个方法之间的区别,优缺点:

<!– 特点 Thread Runnable Callable
优点 方便实现,代码简单 每个线程对象都可以被Thread类对象重复启动。
实现接口,线程类就还能继承其它类,线程之间资源共享方便,不用加static进行修饰
实现接口,线程类就还能继承其它类,线程之间资源共享方便,不用加static进行修饰,线程可以有返回值,线程可以抛出异常
缺点 每个线程对象都只能启动一次
必须重写run方法run,方法不能有返回值,run方法不能抛出异常
必须重写run方法,run方法不能有返回值,run方法不能抛出异常 线程创建比较麻烦,代码比较复杂 –>

线程状态:

一共有T1、T2、T3三个时间段:

1
2
3
4
5
6
7
启动 	T1

运行
等待 消耗小 T2
计时等待

终止 T3

线程池:– 减少 T1 T3 所占的时间和资源
在这里插入图片描述

线程安全

        如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的
这里以小球绘制为例:
背景:给画板添加点击事件,点击一次画板屏幕则创建一个小球线程
不加锁的情况下:
面板绘制主函数:
给画板添加点击事件ballLiten

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BallUI extends JFrame{

BallUI(){
setTitle("运动小球");
setSize(600,600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
Graphics g = getGraphics();// 获取画笔对象
BallListener ballLiten =new BallListener(g);
addMouseListener(ballLiten);

}
public static void main(String[] args) {
new BallUI();
}
}

重写点击方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BallListener implements MouseListener{
/**
* 点击一次 创建一个球 运动
*/
Graphics g;

BallListener(Graphics g){
this.g = g;
}
Random random = new Random();
@Override
public void mouseClicked(MouseEvent e) {
Color color = new Color(random.nextInt(Integer.MAX_VALUE/200));
DrawBallThread dbt = new DrawBallThread(g, e.getX(), e.getY(), 30,color);
dbt.num=0;
Thread t = new Thread(dbt);
t.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
51
52
53
54
/**
* implements Runnable 每个线程 实际运行的内容是独立的
* 成员属性是共享的
* @author Administrator
*
*/
public class DrawBallThread implements Runnable {
Graphics g;// 保证可见
int x = 300;
int y = 300;
int size = 50;
Color color = Color.RED;

public DrawBallThread(Graphics g, int x, int y, int size, Color color) {
this.g = g;
this.x = x;
this.y = y;
this.size = size;
this.color = color;
}
int num;
int speedX=5;

@Override
public void run() {
int copynum = num;
// Random ran = new Random();
while (true) {
//-------------------------不加锁的情况下--------------------
//g.setColor(new Color(238,238,238)); //灰色 先画背景 相当于清屏
//g.fillOval(x, y, size, size);
if (copynum ** 0) {
if(x<0||x>500) {
speedX =-speedX;
}
if(y<0||y>500) {
speedX =-speedX;
}
x += speedX;
y += speedX;
}
g.setColor(color);
g.fillOval(x, y, size, size);
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

}

分析
由图可见,在线程执行的方法中由于每个点击事件触发的线程拿到的都是同一个Graphics g对象,在一个线程启动后不久,另外点击而新开的线程拿到的仍然是同一个Graphics g对象,这里的Graphics g对象属于共享变量,它的color属性也是在不断变化的!从而导致各个线程之间的球颜色混乱!如果要解决此问题,则要将线程执行方法中的Graphics g对象这个共享变量加锁,保证它在当前线程运行结束的情况下才可被其他线程所调用,如下为加锁情况:

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
54
55
56
57
58
59
60
61
62
63
64
/**
* implements Runnable 每个线程 实际运行的内容是独立的
* 成员属性是共享的
* @author Administrator
*
*/
public class DrawBallThread implements Runnable {
Graphics g;// 保证可见
int x = 300;
int y = 300;
int size = 50;
Color color = Color.RED;

public DrawBallThread(Graphics g, int x, int y, int size, Color color) {
this.g = g;
this.x = x;
this.y = y;
this.size = size;
this.color = color;
}
int num;
int speedX=5;

@Override
public void run() {
int copynum = num;
// Random ran = new Random();
while (true) {
//-------------------------加锁的情况下-------------------- synchronized (g) {
// g.setColor(new Color(238,238,238)); //灰色 先画背景 相当于清屏
// g.fillOval(x, y, size, size);
// }
if (copynum ** 0) {
if(x<0||x>500) {
speedX =-speedX;
}
if(y<0||y>500) {
speedX =-speedX;
}
x += speedX;
y += speedX;
}

synchronized (g) {
// g.setColor(Color.white);
// g.fillRect(0, 0, 500, 500);

g.setColor(color);
// for (int i = 0; i < 300; i++) {
// g.fillOval(x, y, size, size);
// }
g.fillOval(x, y, size, size);

}
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

小结:
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

同步代码块、锁

        前面先引出了小球的案例,已对锁有了初步了解,现在详细介绍一下同步代码块、锁:
        当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
        要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制 (synchronized)来解决。

同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:

1
2
 synchronized(同步锁){ 需要同步操作的代码
}

同步锁: 对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

    注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。

同步方法: 使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外
等着。

1
2
 public synchronized void method(){ 可能会产生线程安全问题的代码
}

Lock锁java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。 Lock锁也称同步锁,加锁与释放锁方法化了,如下:

1
2
public void lock() :加同步锁。 
public void unlock() :释放同步锁。

举例:
在线程执行方法的前后执行锁对象的lock、unlock方法达到加锁和释放锁的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Ticket implements Runnable{ private int ticket = 100;
Lock lock = new ReentrantLock(); /*
* 执行卖票操作
*/
@Override
public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作 //使用sleep模拟一下出票时间 try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace(); }
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
}
lock.unlock();
}
} }

几个安全相关的概念普及:

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。

可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。常采用volatile去修饰变量,能够保证各个线程的一致可见性,也就是当一个线程修改了变量值后,别的线程能够看到

有序性:即程序执行的顺序按照代码的先后顺序执行。

num=1;  -- 1 num++  2 num++  数据安全不能保证 

volatile    -- 一致可见性 不能保证线程数据安全

synchronized -- 重量级锁 性能优秀 -- 不方便 -- 实现锁机制 --  保证原子性 

final 

原子操作:载入 堆中共享变量  写回变量  lock unlock 

    保证数据原子性的操作:
        一个个来
    文件操作:
        读 写 
    所有线程 只读 -- 共享 
    线程 一个读   一个写 -- 必须实现 锁 排队 
    所有线程  写 写 - 必须实现 锁        

JUC     
ArrayList -- 增删查改 -- 原子操作的是:增删改 
Redis -- 单线程 

方法 :
分别对哪些资源对象进行加锁 
    方法中使用的变量 
    方法所在类的对象