Catalog
  1. 1. Java多线程与IO流学习笔记(二)
    1. 1.1. 多线程
      1. 1.1.1. 并发与并行
      2. 1.1.2. 线程和进程
      3. 1.1.3. 线程调度
      4. 1.1.4. 创建线程类
      5. 1.1.5. 多线程原理
      6. 1.1.6. Thread类常用方法
      7. 1.1.7. 创建线程的第二种方式
      8. 1.1.8. Thread和Runnable的区别
      9. 1.1.9. 匿名内部类的方式实现线程的创建
    2. 1.2. 线程安全
      1. 1.2.1. 线程同步
      2. 1.2.2. 同步代码块
      3. 1.2.3. 同步方法
      4. 1.2.4. Lock锁
Java多线程与线程安全

Java多线程与IO流学习笔记(二)

多线程

并发与并行

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

    线程和进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

线程调度

  • 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用cpu的时间。
  • 抢占式调度:优先让优先级高的线程使用cpu,如果进程的优先级相同,那么会随机选择一个(线性随机性),Java使用的为抢占式调度。

创建线程类

Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是 完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。 Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

1
2
3
4
5
6
7
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run-->" + i);
}
}
}

测试类:

1
2
3
4
5
6
7
8
9
public class Demo01Thread {
public static void main(String[] args) {
MyThread md = new MyThread();
md.start();
for (int i = 0; i < 20; i++) {
System.out.println("main-->" + i);
}
}
}

多线程原理

程序启动运行main的时候,Java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。 多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明: 多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

Thread类常用方法

  • public String getName():获取当前线程名称
  • public void start():导致次线程开始执行;Java虚拟机调用此线程的run方法
  • public void run():此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停
  • public static Thread currentThread():返回当前正在执行的线程对象的引用

创建线程的第二种方式

采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。 步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

代码如下:

1
2
3
4
5
6
7
8
public class RunnableImpl implements Runnable {
@Override
public void run() {
for(int i =0;i<20;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
public class Demo01Runnable {
public static void main(String[] args) {
RunnableImpl runnableImpl = new RunnableImpl();
Thread t = new Thread(runnableImpl);
t.start();
for(int i =0;i<20;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}

Thread和Runnable的区别

实现Runnable接口创建多线程的好处:

  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
/**
* 匿名内部类的作用:简化代码 把子类继承父类,重写父类的方法,创建子类对象合一步完成 把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
* 匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
*
* 格式: new 父类/接口(){ 重写父类/接口中的方法 }
*/
public class Demo01InnerClassThread {
public static void main(String[] args) {
// 线程的父类是Thread
new Thread() {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + "zycode");
}
}
}.start();

//线程的接口是Runnable
new Thread(new Runnable() {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + "NWPU");
}
}
}).start();
}
}

线程安全

多线程访问了共享的数据会产生线程安全问题。

案例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 实现买票案例
*/
public class RunnableImpl implements Runnable {
private int ticket = 100;

@Override
public void run() {
while(true){
if(ticket > 0){
//提高安全问题出现的概率
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->正在买第"+ticket+"张票");
ticket--;
}
}
}
}

主方法:

1
2
3
4
5
6
7
8
9
10
11
public class Demo01Tickket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}

出现结果:

过程图示:

线程同步

解决线程安全问题可以使用线程同步机制(synchronized)来解决。

有三种同步操作:

  • 同步代码块
  • 同步方法
  • 锁机制

同步代码块

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

格式:

1
2
3
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
    public class RunnableImpl implements Runnable {
    private int ticket = 100;

    // 创建一个锁对象
    Object obj = new Object();

    @Override
    public void run() {
    while (true) {
    synchronized (obj) {
    if (ticket > 0) {
    // 提高安全问题出现的概率
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "-->正在买第" + ticket + "张票");
    ticket--;
    }
    }
    }
    }
    }
    同步技术的原理:

使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器。

3个线程一起抢夺CPU的执行权,谁抢到了谁执行run方法进行卖票,t0抢到了CPU的执行权,执行run方法,遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行。

t1抢到了CPU的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象,发现没有,t1就会进入到阻塞状态,会一直等待t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

问题:程序频繁的判断锁,获取锁,释放锁,程序的效率会降低。

同步方法

  • 同步方法: 使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外
    等着。
    格式:
    1
    2
    3
    public synchronized void method(){
    //可能会产生线程安全问题的代码
    }
    步骤:
  1. 把访问了共享数据的代码抽取出来,放入一个方法中
  2. 在方法上添加synchronized关键字

同步方法的锁对象是谁?就是实现类对象,new RunnableImpl(),也就是this。

静态的同步方法:

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

静态方法的锁对象是本类的class属性–>class文件对象(反射)

Lock锁

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

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

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

使用步骤:

  1. 在成员位置创建一个ReentrantLock对象
  2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
  3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
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
public class RunnableImpl implements Runnable {
private int ticket = 100;

// 创建一个锁对象
Lock l = new ReentrantLock();

@Override
public void run() {
while (ticket>0) {
l.lock();
if (ticket > 0) {
// 提高安全问题出现的概率
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "-->正在买第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
l.unlock(); //无论程序是否异常都会把锁释放掉
}
}
}
}
}
Author: zycode1561
Link: https://zycode1561.github.io/2020/01/14/Java%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
  • 支付宝

Comment