五、并发编程【17-18】

128 阅读9分钟

java基础学习的文章目录

1、多线程原理_默认线程

  • 什么是进程?
    • 在操作系统中运行的一个应用程序
  • 比如同时打开QQ、微信,操作系统就会分别启动2个进程
  • 每个进程之间是独立的, 每个进程均运行在其专用且受保护的内存空间内
  • 在 Windows 中,可以通过“任务管理器”查看正在运行的进程

线程

  • 什么是线程?
    • 1 个进程要想执行任务,必须得有线程(每 1 个进程至少要有 1 个线程)
    • 一个进程的所有任务都在线程中执行

线程的串行

  • 1 个线程中任务的执行是串行的
    • 如果要在 1 个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务
    • 在同一时间内, 1 个线程只能执行 1 个任务

多线程

  • 什么是多线程
    • 1 个进程中可以开启多个线程,所有线程可以并行(同时) 执行不同的任务
    • 多线程技术可以提高程序的执行效率

多线程的原理

  • 同一时间, CPU 的 1 个核心只能处理 1 个线程(只有 1 个线程在工作)
  • 多线程并发(同时)执行,其实是 CPU 快速地在多个线程之间调度(切换)
  • 如果 CPU 调度线程的速度足够快,就造成了多线程并发执行的假象
  • 如果是多核 CPU,才是真正地实现了多个线程同时执行
  • 思考:如果线程非常非常多,会发生什么情况?
    • CPU 会在 N 个线程之间调度,消耗大量的 CPU 资源, CPU 会累死
    • 每条线程被调度执行的频次会降低(线程的执行效率降低)

多线程的优缺点

  • 优点
    • 能适当提高程序的执行效率
    • 能适当提高资源利用率(CPU、内存利用率)
  • 缺点
    • 开启线程需要占用一定的内存空间, 如果开启大量的线程, 会占用大量的内存空间,降低程序的性能
    • 线程越多, CPU 在调度线程上的开销就越大
    • 程序设计更加复杂
      • 比如线程之间的通信问题、多线程的数据共享问题

默认线程

  • 每一个 Java 程序启动后,会默认开启一个线程,称为主线程(main 方法所在的线程)
  • 每一个线程都是一个 java.lang.Thread 对象,可以通过 Thread.currentThread 方法获取当前的线程对象

2、开启新线程

开启线程的方式1:

public class Main {
	public static void main(String[] args) {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				//开启了新线程com.lijian.testThread
				System.out.println("开启了新线程" + Thread.currentThread().getName());
			}
		});
		thread.setName("com.lijian.testThread");
		thread.start();
	}
}

这种方式是给创建一个thread,并传递一个Runnable接口

开启线程的第二种方法

public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("开启了新线程 " + Thread.currentThread().getName());
	}
	public static void main(String[] args) {
		MyThread thread = new MyThread();
		//开启了新线程 com.lijian.testThread
		thread.setName("com.lijian.testThread");
		thread.start();
	}
}
  • 注意
    • 直接调用线程的 run 方法并不能开启新线程
    • 调用线程的 start 方法才能成功开启新线程
  • Thread 类实现了 Runnable 接口

3、多线程的内存布局

  • PC 寄存器(Program Counter Register):每一个线程都有自己的 PC 寄存器
  • Java 虚拟机栈(Java Virtual Machine Stack):每一个线程都有自己的 Java 虚拟机栈
  • 堆(Heap):多个线程共享堆
  • 方法区(Method Area):多个线程共享方法区
  • 本地方法栈(Native Method Stack):每一个线程都有自己的本地方法栈

4、线程的状态

  • 可以通过 Thread.getState 方法获得线程的状态(线程一共有 6 种状态)
  • New(新建):尚未启动
  • Runnable(可运行状态):正在 JVM 中运行 * 或者正在等待操作系统的其他资源(比如处理器)
  • Blocked(阻塞状态):正在等待监视器锁(内部锁)
  • waiting(等待状态):在等待另一个线程
    • 调用以下方法会处于等待状态
      • 没有超时值的 Object.wait
      • 没有超时值的 Thread.join
      • LockSupport.park
  • Timed_waiting(定时等待状态)
    • 调用以下方法会处于定时等待状态
      • Thread.sleep
      • 有超时值的 Object.wait
      • 有超时值的 Thread.join
      • LockSupport.parkNanos
      • LockSupport.parkUntil
  • Terminated(终止状态):已经执行完毕

线程间状态切换

                      Timed_Waiting
                
New ->Runnable    ⥦   Blocked
         ↓
       Terminated     Waiting

5、sleep_interrupt

  • 可以通过 Thread.sleep 方法暂停当前线程,进入 waiting状态
    • 在暂停期间,若调用线程对象的 interrupt 方法中断线程,会抛出 java.lang.InterruptedException 异常

示例代码

	public static void main(String[] args) {
		Thread thread = new Thread(()->{
			System.out.println("begin");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println("interrupt");
			}
			System.out.println("end");
		} ); 
		
		thread.start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		thread.interrupt();
	}

输出:

begin
interrupt
end

6、join_isAlive

  • A.join 方法:等线程 A 执行完毕后,当前线程再继续执行任务。可以传参指定最长等待时间
  • A.isAlive 方法:查看线程 A 是否还活着 示例代码:
	public static void main(String[] args) {
		Thread t1 = new Thread(()->{
			System.out.println("t1 - begin");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				System.out.println("interrupt");
			}
			System.out.println("t1 - end");
		} ); 
		t1.start();
		
		
		Thread t2 = new Thread(()->{
			System.out.println("t2 - begin");
			System.out.println("t1.isAlive- " + t1.isAlive());
			try {
				t1.join();
			} catch (InterruptedException e) {
				System.out.println("interrupt");
			}
			System.out.println("t1.isAlive- " + t1.isAlive());
			System.out.println("t1.state " + t1.getState());
			System.out.println("t2 - end");
		} ); 
		t2.start();
	}

示例代码输出:

t1 - begin
t2 - begin
t1.isAlive- true
t1 - end
t1.isAlive- false
t1.state TERMINATED
t2 - end

7、线程安全01_问题

  • 多个线程可能会共享(访问)同一个资源
    • 比如访问同一个对象、 同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题,称为线程安全问题
  • 什么情况下会出现线程安全问题?
    • 多个线程共享同一个资源
    • 且至少有一个线程正在进行写的操作

线程安全问题演示

public class Station implements Runnable {
	private int tickets = 100;
	public boolean saleTicket() {
		if (tickets < 1) {
			return false;
		}
		tickets--;
		String name = Thread.currentThread().getName();
		System.out.println(name + "卖了一张,剩余" + tickets + "张");
		return tickets > 0;
	}

	@Override
	public void run() {
		while (saleTicket()) ;
	}
	
	public static void main(String[] args) {
		Station station = new Station();
		for (int i = 0; i < 4; i++) {
			Thread th = new Thread(station);
			th.setName("" + i);
			th.start();
		}
	}
}

部分输出:

3卖了一张,剩余15张
2卖了一张,剩余16张
1卖了一张,剩余17张
2卖了一张,剩余12张
3卖了一张,剩余13张
0卖了一张,剩余14张
0卖了一张,剩余8张
3卖了一张,剩余9张
2卖了一张,剩余10张
2卖了一张,剩余5张
2卖了一张,剩余4张
2卖了一张,剩余3张
1卖了一张,剩余11张
2卖了一张,剩余2张
3卖了一张,剩余6张
0卖了一张,剩余7张
2卖了一张,剩余0张
1卖了一张,剩余1张
  • 可以使用线程同步技术来解决线程安全问题
    • 同步语句(Synchronized Statement)
    • 同步方法(Synchronized Method)

8、线程安全02_同步语句

	public boolean saleTicket() {
		synchronized (this) {
			if (tickets < 1) {
				return false;
			}
			tickets--;
			String name = Thread.currentThread().getName();
			System.out.println(name + "卖了一张,剩余" + tickets + "张");
			return tickets > 0;
		}
	}

同步后的部分输出

0卖了一张,剩余10张
0卖了一张,剩余9张
0卖了一张,剩余8张
0卖了一张,剩余7张
0卖了一张,剩余6张
0卖了一张,剩余5张
0卖了一张,剩余4张
0卖了一张,剩余3张
0卖了一张,剩余2张
0卖了一张,剩余1张
0卖了一张,剩余0张
  • synchronized obj) 的原理
    • 每个对象都有一个与它相关的内部锁(intrinsic lock)或者叫监视器锁(monitor lock)
    • 第一个执行到同步语句的线程可以获得 obj 的内部锁,在执行完同步语句中的代码后释放此锁
    • 只要一个线程持有了内部锁,那么其它线程在同一时刻将无法再获得此锁
      • 当它们试图获取此锁时,将会进入 BLOCKED状态
  • 多个线程访问同一个 synchronized obj) 语句时
    • obj 必须是同一个对象,才能起到同步的作用

9、线程安全03_同步方法

	public synchronized boolean saleTicket() {
		if (tickets < 1) {
			return false;
		}
		tickets--;
		String name = Thread.currentThread().getName();
		System.out.println(name + "卖了一张,剩余" + tickets + "张");
		return tickets > 0;
	}
  • synchronized 不能修饰构造方法
  • 同步方法的本质
    • 实例方法: synchronized this)
    • 静态方法: synchronized Class对象)
  • 同步语句比同步方法更灵活一点
    • 同步语句可以精确控制需要加锁的代码范围
  • 使用了线程同步技术后
    • 虽然解决了线程安全问题,但是降低了程序的执行效率
    • 所以在真正有必要的时候,才使用线程同步技术

10、单例模式改进_细节

public class Rocket {
    private static Rocket instance = null;
    private Rocket(){} // 构造函数私有化
    
    //单例方法
    public static synchronized Rocket getInstance() {
        if(instance == null) {
            instance = new Rocket();
        }
        return instance;
    }
    
}

几个常用类的细节

  • 动态数组
    • ArrayList:非线程安全
    • Vector:线程安全
  • 动态字符串
    • StringBuilder:非线程安全
    • StringBuffer:线程安全
  • 映射(字典)
    • HashMap:非线程安全
    • Hashtable:线程安全

11、死锁

  • 什么是死锁?
    • 两个或者多个线程永远阻塞,相互等待对方的锁
	public static void main(String[] args) {
		Thread t1 = new Thread(()->{
			synchronized ("1") {
				System.out.println("1  --- 1");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized ("2") {
					System.out.println("1  --- 2");
				}
			}
		} ); 
		t1.start();
		
		Thread t2 = new Thread(()->{
			synchronized ("2") {
				System.out.println("2  --- 1");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized ("1") {
					System.out.println("2  --- 2");
				}
			}
		} ); 
		t2.start();
	}

死锁示例代码输出:

1  --- 1
2  --- 1

死锁示例:

public class Persion {
	private String name;

	public Persion(String name) {
		super();
		this.name = name;
	}
	
	public synchronized void hello(Persion p) {
		System.out.format("[%s] hello to [%s]%n", name, p.name);
		p.smile(this);
	}

	public synchronized void smile (Persion p) {
		System.out.format("[%s] smile to [%s]%n", name, p.name);
	}
	
	public static void main(String[] args) {
		Persion jack = new Persion("Jack");
		Persion rose = new Persion("Rose");
		new Thread(()-> {jack.hello(rose);} ).start();  
		new Thread(()-> {rose.hello(jack);} ).start();  
	}
}

输出:

[Jack] hello to [Rose]
[Rose] hello to [Jack]

13、线程间通信01

  • 可以使用 Object.wait、 Object.notify、 Object.notifyAll 方法实现线程之间的通信
  • 若想在线程 A 中成功调用 obj.wait、 obj.notify、 obj.notifyAll 方法
    • 线程 A 必须要持有 obj 的内部锁
  • obj.wait :释放 obj 的内部锁,当前线程进入Waiting 或 TimeWaiting状态
  • obj.notifyAll :唤醒所有因为 obj.wait 进入 当前线程进入Waiting或 TimeWaiting状态状态的线程
  • obj.notify :随机唤醒 1 个因为 obj.wait 进入当前线程进入Waiting 或 TimeWaiting状态状态的线程 状态的线程

示例代码Drop

package lession18;

public class Drop {
	private String food;
	// empty 为true 表示消费者等待生产者
	// empty为false,有内容,生产者等待消费者
	private boolean empty = true;
	
	public synchronized String get() {
	   // 消费者线程拿到了drop对象的内部锁
		while (empty) {
			// 等待生产者生产食物 
			try {
			    //消费者线程会释放drop对象的内部锁,然后进入waiting状态
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		empty = true;
		notifyAll();
		return food;
	}
	
	public synchronized void add(String food) {
		while (!empty) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		empty = false;
		this.food = food;
		notifyAll();
	}
}

消费者代码:

public class Consumer implements Runnable {
	private Drop drop;

	public Consumer(Drop drop) {
		super();
		this.drop = drop;
	}
	
	@Override
	public void run() {
		String food = null;
		while ((food = drop.get()) != null) {
			System.out.println("消费者拿到食物" + food);
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

生产者

public class Producer implements Runnable {
	private Drop drop;

	public Producer(Drop drop) {
		this.drop = drop;
	}
	
	@Override
	public void run() {
		String[] foods = {
				"beef",
				"bread",
				"apple",
				"cookie",
				"banana"
		};
		
		for (int i = 0; i < foods.length; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			drop.add(foods[i]);
		}
		
		drop.add(null);
		
	}
}

测试代码:

public class Main {
	public static void main(String[] args) {
		Drop drop = new Drop();
		new Thread(new Consumer(drop)).start();
		new Thread(new Producer(drop)).start();
		
		/*
		 * 调用wait,notify必须是同一个obj对象
		 * 调用wait、notify的线程必须拥有obj对象的内部锁
		 */
	}
}

16、ReentrantLock

  • ReentrantLock ,译为“可重入锁”

    • 类的全名是: java.util.concurrent.locks.ReentrantLock
    • 具有跟同步语句、同步方法一样的一些基本功能,但功能更加强大
  • 什么是可重入?

    • 同一个线程可以重复获取同一个锁
    • 其实 synchronized 也是可重入的
  • ReentrantLock.lock :获取此锁

    • 如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回
    • 如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回
    • 如果此锁被另一个线程持有,并且在获得锁之前,此线程将一直处于休眠状态,此时锁的持有计数被设为 1
  • ReentrantLock.tryLock :仅在锁未被其他线程持有的情况下,才获取此锁

    • 如果此锁没有被另一个线程持有,则将锁的持有计数设为 1,并且此方法立即返回 true
    • 如果当前线程已经持有此锁,则将锁的持有计数加 1,并且此方法立即返回 true。
    • 如果锁被另一个线程持有,则此方法立即返回 false
  • ReentrantLock.unlock :尝试释放此锁

    • 如果当前线程持有此锁,则将持有计数减 1
    • 如果持有计数现在为 0,则释放此锁
    • 如果当前线程没有持有此锁,则抛出 java.lang.IllegalMonitorStateException
  • ReentrantLock.isLocked :查看此锁是否被任意线程持有

使用示例:

	private ReentrantLock lock = new ReentrantLock();
	public  boolean saleTicket() {
		lock.lock();
		try {
			if (tickets < 1) {
				return false;
			}
			tickets--;
			String name = Thread.currentThread().getName();
			System.out.println(name + "卖了一张,剩余" + tickets + "张");
			return tickets > 0;
		} finally {
			lock.unlock();
		}
	}

可重入的理解代码

		synchronized ("1") {
			synchronized ("1") {
				System.out.println("可重入测试");
			}
		}

17、线程池

  • 线程对象占用大量内存,在大型应用程序中,频繁地创建和销毁线程对象会产生大量内存管理开销
  • 使用线程池可以最大程度地减少线程创建、销毁所带来的开销
  • 线程池由工作线程(Worker Thread)组成
    • 普通线程:执行完一个任务后,生命周期就结束了
    • 工作线程:可以执行多个任务(任务没来就一直等,任务来了就干活)
      • 先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中
  • 常用的线程池类型是固定线程池(Fixed Thread Pool)
    • 具有固定数量的正在运行的线程

示例代码:

	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(5);
		pool.execute( ()->{
			System.out.println(11 + "_" + Thread.currentThread().getName());
		} );
		
		pool.execute( ()->{
			System.out.println(22 + "_" + Thread.currentThread().getName());
		} );
		
		pool.execute( ()->{
			System.out.println(33 + "_" + Thread.currentThread().getName());
		} );
		pool.shutdown();
	}

线程状态Blocked和Waiting的区分

一个线程如果正在执行代码(任务),就会消耗CPU时间片

  • Blocked状态等待锁:类似于while,会消耗时间片
  • Waitting状态: 不会消耗时间片