Java之并发编程笔记
文章目录
- Java之并发编程笔记
创作不易,希望各位看官点赞收藏。
Java之并发编程笔记
程序:完成特定任务,用某种语言编写的指令集合,就是一段静态代码块。
进程:是程序的一次进行过程,或者是正在执行的程序,一个动态过程。
线程:进程可以进一步细分成线程,是程序内部的一条执行路径。线程作为调度和执行单位,每一个线程都拥有自己的独立运行栈和程序计数器,线程的切换小。一个进程可能有多个线程,线程共享进程的资源。方法区和堆是每一个进程一份,虚拟机栈和程序计数器是每一个线程一份。一个Java程序启动至少有三个线程,main主线程、
GC
垃圾回收线程、异常处理线程。
并行:多个CPU同时执行多个任务。
并发:一个CPU执行多个任务,只有一个任务占用CPU,采用时间片切换执行的任务。
- 线程就是独立的执行路径。
- 程序运行时,即使没有创建自己的线程,后台也有多个线程。
- main()为程序的主线程,为系统的入口。
- 在一个进程中,如果存在多个线程,线程的运行由CPU调度,不能人为干预。
- 对同一份资源进行操作时,存在资源抢夺的问题,并发问题。
- 线程会带来额外的资源消耗。
- 每个线程在自己的工作内存交互,内存数据控制不当会造成数据不一致的问题。
一个程序会有一个进程,一个进程中可能包含多个线程,至少有一个线程。
1、线程
1.1、线程的创建
线程的创建有三种方式:①、继承Thread类;②、实现Runnable接口;③、实现Callable接口;
继承Thread类:
- 继承Thread类,并重写run()方法,将这个线程的执行方法声明在这个方法中。
- 创建子类对象,并调用start()方法来启动这个线程。
public class MyThread extends Thread{ // 重写父类中的run方法 @Override public void run() { for(int i=0;i<10;i++){ System.out.println("我是新线程中的方法"); } }}public static void main(String[] args) { // 创建一个Thread子类对象,并启动线程 Thread thread = new MyThread(); thread.start(); for(int i=0;i<50;i++){ System.out.println("我是main线程"); }}
start()方法的作用:①启动这个线程;②执行这个线程中重写的run()方法。
注意事项:
- 不能直接调用子类的run()方法,这样不会报错,但是不会创建一个线程,而是按照方法调用main线程帮你执行的。
- 对于一个线程不能调用两次start()方法,只能新创建一个线程对象在调用start()方法。
实现Runnable接口:
- 创建一个类并实现Runnable接口,并实现其中的run()方法。
- 创建一个实现类的对象。
- 将实现类作为参数,创建一个Thread类。
public class RunnableTest implements Runnable{ // 重写run方法 @Override public void run() { for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+" = "+i); } } public static void main(String[] args) { // 创建接口实现类 Runnable runnable1 = new RunnableTest(); Runnable runnable2 = new RunnableTest(); // 创建线程,这里有一个重载,可以指定线程的名称 Thread thread1 = new Thread(runnable1, "线程A"); Thread thread2 = new Thread(runnable1); // 启动线程 thread1.start(); thread2.start(); }}
1.2、Thread常用方法
void start(); // 启动线程并执行线程的run方法String getName(); // 获取线程的名称void setName(); // 设置线程的名称static Thread currentThread(); // 获取当前线程对象static void yield(); // 释放当前线程的CPU资源,但是不一定是让给其他线程使用,CPU资源可能再次分配给这个线程static void join(); // 当在a线程中调用b线程的join()方法,这时线程a进入阻塞状态,等到线程b完全执行完后线程a才结束阻塞状态。void stop(); // 结束调用的线程,这个方法已经过时不在使用static void sleep(long millis); // 指定线程休眠指定时间,单位毫秒,过了这个时间线程自动就绪 void isAlive(); // 判断调用线程是否存活
1.2.1、sleep()
用于将一个线程进入阻塞状态,设置一个时间,当到时间时,线程继续执行。
// 定时器public static void main(String[] args) { for(int i=0;i<=20;i++){ System.out.println("计数器 : "+i); try { // 线程休眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}
注意事项:
- sleep()方法就是将一个线程阻塞指定的毫秒数,当时时间到达时,线程就进入就绪状态。
- sleep()方法可以模拟网络延时和计数器。
- 每一个线程对象都有一个锁,sleep不会释放锁。
1.2.2、yield()
线程礼让,当前线程释放CPU资源,然后从运行状态进入就绪状态,重新和其它线程抢夺资源。线程礼让不一顶成功,如果礼让的线程又成功抢到了CPU的资源,礼让的线程就会继续执行对应的程序,相当于没有礼让。
public static void main(String[] args) { new Thread("线程A"){ @Override public void run() { System.out.println(this.getName()+": 执行开始"); for(int i=0;i<1000;i++){ Thread.yield(); // 由于可能出现礼让不成功,所以多礼让几次 } System.out.println(this.getName()+": 执行结束"); } }.start(); new Thread("线程B"){ @Override public void run() { System.out.println(this.getName()+": 执行开始"); System.out.println(this.getName()+": 执行结束"); } }.start();}
1.2.3、join()
当在a线程中调用b线程的join()方法,这时线程a进入阻塞状态,等到线程b完全执行完后线程a才结束阻塞状态,可以理解为一个线程插队另一个线程。
public static void main(String[] args) { Thread threadA = new Thread("线程A") { @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" = "+i); } } }; Thread threadB = new Thread("线程B") { @Override public void run() { for (int i = 0; i < 20; i++) { if (i==10){ try { // 线程A插队,只有等线程A执行完成,线程B再继续执行 threadA.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" = "+i); } } }; threadA.start(); threadB.start();}
1.3、线程优先级
同等优先级的线程采用先到先服务,使用时间片策略。我们可以设置对应线程的优先级,使用优先调度策略。在线程中有几个常量优先级。优先级是一个1-10的整数,超过这个范围会报错,默认的优先级为5。
public final static int MIN_PRIORITY = 1;public final static int NORM_PRIORITY = 5; // 默认的优先级public final static int MAX_PRIORITY = 10;
System.out.println("线程优先级 === "+thread.getPriority());thread.setPriority(11); // 只能设置1-10,超过这个范围会报错
注意事项:
- 设置为高的优先级并不是该线程比其它线程先执行,而是在CPU分为资源是会大概率先给优先级高的线程。
- 线程优先级的设置必须要在调用start()方法前面,不然没有效果。
1.4、守护线程
守护线程:在Java中线程分为用户线程和守护线程,它们几乎相同只是结束的时间不同,用户线程根据用户需要结束,而守护线程是保证用户线程的正常执行,只有
JVM
中没有了用户线程,守护线程才会结束。Java垃圾回收就是一个守护线程。
public class Demo01 { public static void main(String[] args) { Thread thread = new Thread(new DaemonThread(), "守护线程"); thread.setDaemon(true); // 把线程设置为守护线程,true为守护线程,false为用户线程 thread.start(); for (int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+" = "+i); } // 守护线程是一个死循环,如果在JVM中没有其他用户线程,守护线程自动结束 }}class DaemonThread implements Runnable{ @Override public void run() { while (true){ System.out.println("我是守护线程"); } }}
1.5、线程生命周期
在Thread类中,封装了一个内部枚举类State来表示线程状态:
public enum State { NEW, // 创建状态,new了一个Thread对象(创建状态) RUNNABLE, // 可运行状态,可能是正在执行,也可能是在等待cpu分配资源(就绪状态、运行状态) BLOCKED, // 等待监视器锁的线程的线程状态(阻塞状态) WAITING, // 等待状态,处于等待状态的线程正在等待另一个线程执行特定的操作。(阻塞状态) TIMED_WAITING, // 具有时间的等待状态,例如调用了sleep()、wait(time)(阻塞状态) TERMINATED; // 死亡状态,线程结束(死亡状态)}
getState()
:获取当前线程的状态,返回值是枚举类中的值。
Thread.State state = Thread.currentThread().getState(); // 返回值是枚举类
1.6、线程同步
线程安全问题:当线程a操作某一共享数据时,然后线程a还没操作完成,其它线程也去操作这一共享数据,产生一些意想不到的问题。这是就需要线程同步来解决,线程a操作共享数据时,只有当线程a操作完成后其它线程才能去操作这一共享数据,即使线程a在操作的过程中被sleep了,其它线程也不能去操作这一共享数据。
在Java中通过同步机制解决线程安全问题。为了保证数据的安全性,在访问资源的时候增加锁机制(synchronized),当一个线程获得操作对象的锁时,独占资源其它线程就必须等待,使用完后再释放锁供其他线程使用,这就出现排队现象。一般使用synchronized锁定的是所需要操作的公共资源。
1.6.1、同步代码块
synchronized(obj){ // 需要同步的代码内容,操作共享数据的代码,这些代码一次只允许一个线程执行}
同步监视器:上面的obj就是同步监视器,俗称:锁。谁拥有这个锁都可以操作同步代码块。它必须满足下面的要求:
- 它可以是任意的对象,但是不能是基本数据类型。
- 多个线程必须同用一个同步监视器。
- 通常由private final修饰,例如private final Object lock=new Object();。因为锁句柄值一旦改变会导致执行同一个代码块的多个线程会执行不同锁,从而导致竞态。
- 也可以使用
this
当前对象来作为一个锁。
public class Demo02 { public static void main(String[] args) { Window window = new Window(100); new Thread(window,"窗口1").start(); new Thread(window,"窗口2").start(); new Thread(window,"窗口3").start(); }}class Window implements Runnable{ private Integer tickets; private final Object obj = new Object(); public Window(Integer tickets) { this.tickets = tickets; } @Override public void run() { while(true){ synchronized(obj){ if(tickets > 0){ System.out.println(Thread.currentThread().getName()+" == "+tickets--); }else{ System.out.println("票卖完了!!!!"); break; } } } }}
1.6.2、同步方法
如果操作共享数据的代码刚好在一个方法中,我们就将这个方法声明为同步方法,使用synchronized
修饰方法。
public class Demo03 { public static void main(String[] args) { Account account = new Account("张三", 1000); Draw draw = new Draw(account, 34); new Thread(draw,"线程1").start(); new Thread(draw,"线程2").start(); new Thread(draw,"线程3").start(); }}// 取钱线程class Draw implements Runnable{ private Account account; private Integer money; public Draw(Account account, Integer money) { this.account = account; this.money = money; } // 取钱线程 @Override public void run() { while (true){ pop(this.account,this.money); } } // 取钱线程同步方法 public synchronized void pop(Account account, Integer money){ if (this.account.balance<money){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 账户当前余额不足,当前余额为:"+this.account.balance); }else { account.balance = account.balance - money; System.out.println(Thread.currentThread().getName()+" 取款成功,当前余额为: == "+account.balance); } }}class Account{ String card; Integer balance; public Account(String card, Integer balance) { this.card = card; this.balance = balance; }}
注意事项:
- 同步方法任然存在同步监视器(锁),只是不需要我们显示声明。
- 非静态同步方法的锁是当前对象:this。
- 静态同步方法的锁是类本身:
Obj.class
。 - 在继承方式线程中,同步方法必须是静态的,不然每一个线程的锁不一样。
1.6.3、Lock锁
在Java5.0
之后新增了Lock接口,其子类实现了线程同步解决办法,使用步骤如下:
public class LockTest { public static void main(String[] args) { A a = new A(); new Thread(a,"线程1").start(); new Thread(a,"线程2").start(); new Thread(a,"线程3").start(); }}class A implements Runnable{ private Integer tickets = 100; // 有两个重载构造器:参数为true时,线程采用先来先服务;如果为false采用抢夺资源策略 private Lock lock = new ReentrantLock(); // private Lock lock = new ReentrantLock(true); @Override public void run() { while(true){ try { // 加锁,加锁后面的代码只能是单线程执行 lock.lock(); Thread.sleep(100); if(tickets>0){ System.out.println(Thread.currentThread().getName()+" == "+tickets--); }else { break; } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 执行完以后需要解锁 lock.unlock(); } } }}
- 创建一个
ReentrantLock()
子类。 - 同步代码块使用try-finally块包裹,并在同步代码块之前使用lock为同步代码块加锁。
- 执行完同步代码块后,在finally语句中使用lock为代码块解锁。
- Lock这个锁对象对于多个线程来说必须是同一个对象,在继承中需要静态的Lock对象。
注意:
在Java中解决线程同步主要使用synchronized(同步代码块、同步方法)和使用Lock工具,它们有什么区别?
- 相同点:都能解决线程同步问题。
- 不同点:synchronized在执行完对应的同步代码块以后自动释放锁,而Lock需要手动加锁和手动解锁。
对于这两种方法,选择使用顺序是:Lock -> 同步代码块 -> 同步方法
1.7、死锁
死锁:不同线程占用对象需要的的同步资源不放(互相拥有对方需要的锁),都等待对方释放自己需要的锁,从而导致线程一直在阻塞状态无法结束,但是不会报错也不会有任何提示。
产生死锁的条件:
- 一个资源每次只能被一个线程使用。
- 一个进程因请求资源而进行阻塞,对象的资源保持不放。
- 进程获得的资源,在使用完之前,不能强制剥夺。
- 若干线程之间形成一种头尾相接循环等待资源。
public class Demo04 { public static void main(String[] args) { // 模拟两个资源 StringBuilder s1 = new StringBuilder(); StringBuilder s2 = new StringBuilder(); // 线程1 new Thread(){ @Override public void run() { // 先握s1锁 synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); // 线程2 new Thread(new Runnable() { @Override public void run() { // 先握s2锁 synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); }}
上面的代码出现死锁的情况,因为如果线程1先执行,会先锁住s1
资源然后进入睡眠,这是线程2再执行锁住s2
进入睡眠,然后线程1继续执行发现需要资源s2
被线程2锁住,导致进入阻塞状态。线程2再执行发现需要资源s1
被线程1锁住进入阻塞状态。这样线程1和线程2都在等待对象线程释放对方需要的资源。
1.8、线程通信
线程通信:线程与线程之间可以相互通信,当一个线程执行完以后通知另一个线程执行完毕,这就是线程之间的通信。
// 使用两个线程交替答应1-100的数public class Demo01 { public static void main(String[] args) { Action action = new Action(); new Thread(action,"线程1").start(); new Thread(action,"线程2").start(); }}class Action implements Runnable{ private int num = 1; @Override public void run() { while (true){ synchronized (this){ notify(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (num<=100){ System.out.println(Thread.currentThread().getName()+" == "+num++); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { break; } } } }}
线程通信方法:
wait()、notif()、notifyAll()
- wait:线程等待,释放锁,线程进入阻塞状态。
- notify:唤醒一个wait线程,让这个线程从阻塞状态进入就绪状态,如果有多个wait的线程,就唤醒优先级高的线程。
notifyAll
:唤醒所有wait线程,让这些线程进入就绪状态。
注意事项:
- 线程通信方法只能在同步代码块、同步方法中执行,不能在Lock中使用。
- wait方法会释放同步中的锁给其他线程使用。
- 线程通信方法不是定义在Thread类中,而是定义在Object这个祖先类中的。
- 线程通信方法的调用者必须是锁对象,锁可以是任何对象,所以说这些对象拥有Object类中的通信方法,如果调用者不是锁对象就会报错。
class Action implements Runnable{ private int num = 1; private final Object object = new Object(); @Override public void run() { while (true){ synchronized (object){ // 锁对象是object // notify(); 错误,这种方式的调用者是this对象,出现java.lang.IllegalMonitorStateException异常 object.notify(); // 使用锁对象来调用 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (num<=100){ System.out.println(Thread.currentThread().getName()+" == "+num++); try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { break; } } } }}
sleep() 和 wait()方法的区别:
- 相同点:两个方法都会让线程进入阻塞状态。
- 不同点:
- sleep() 在Thread类中声明,wait()声明在Object类中。
- sleep() 在任何地方都可以使用,wait() 只能在同步代码块、同步方法中使用。
- 如果都在同步代码块、同步方法中使用,sleep() 不会释放锁,而wait() 会释放锁。
1.9、生产者与消费者
生产者与消费者共享一个资源,并且生产者与消费者之间存在依赖关系,只有生产者生产了产品消费者才能消费,而只有消费者消费了,生产者才继续生产,互相作为条件。
缓冲区:作为生产者与消费者之间的共享数据。
public class BufferRegion { private static final int CAPACITY = 20; private int length = 0; // 放入方法 public synchronized void push() throws InterruptedException { // 放入 if (length<CAPACITY){ System.out.println(Thread.currentThread().getName()+"生产了产品,现有:"+(++length)); // 通知消费者消费 this.notify(); }else{ // 容量已满,生产线程停止 System.out.println("容量已满,消费者消费"); this.wait(); } } // 消费方法 public synchronized void pop() throws InterruptedException { // 如果产品数量为0,通知生产者生产 if (length==0){ System.out.println("产品已空,生产者生产"); this.wait(); }else{ // 消费产品 System.out.println(Thread.currentThread().getName()+"消费了产品,现有:"+(--length)); // 通知生产者生产 this.notify(); } }}
生产者:操作缓冲区,向缓冲区中添加数据。
// 生产者public class Producer implements Runnable{ private BufferRegion bufferRegion; public Producer(BufferRegion bufferRegion) { this.bufferRegion = bufferRegion; } @Override public void run() { while (true){ try { // 生产产品 bufferRegion.push(); } catch (InterruptedException e) { e.printStackTrace(); } } }}
消费者:操作缓冲区,消费产品。
// 消费者public class Consumer implements Runnable{ private BufferRegion bufferRegion; public Consumer(BufferRegion bufferRegion) { this.bufferRegion = bufferRegion; } @Override public void run() { while(true){ try { // 消费产品 bufferRegion.pop(); } catch (InterruptedException e) { e.printStackTrace(); } } }}
测试:
// 测试类public class MyTest { public static void main(String[] args) { BufferRegion bufferRegion = new BufferRegion(); new Thread(new Producer(bufferRegion),"生产者").start(); new Thread(new Consumer(bufferRegion),"消费者1").start(); new Thread(new Consumer(bufferRegion),"消费者2").start(); new Thread(new Consumer(bufferRegion),"消费者3").start(); }}
1.10、Callable创建线程
在JDK5.0
之后有了新的创建线程的方式,使用Callable来创建线程:
public class CallableThread { public static void main(String[] args) { // 3、常见实现类对象 NumberSum numberSum = new NumberSum(); // 4、将实现类对象作为构造参数,创建FutureTask对象 FutureTask<Integer> task = new FutureTask<>(numberSum); // 5、将FutureTask作为构造参数创建Thread对象,并启动线程 new Thread(task,"线程A").start(); // 这是主线程执行,获取线程A执行完成后的返回值 try { Integer result = task.get(); System.out.println(Thread.currentThread().getName()+" 获取结果为:"+result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }}// 1、创建一个实现类实现Callable接口class NumberSum implements Callable<Integer> { ///2、重写call方法,线程执行的语句就在这个方法中 @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { if (i%2==0){ Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" == "+i); sum+=i; } } return sum; }}
具体步骤:
- 创建一个实现Callable接口的实现类,并实现 call() 方法,把线程执行语句写在 call() 方法中。
- 创建一个实现类对象,并将对象作为构造参数创建一个
FutureTask
对象。 - 将
FutureTask
对象作为参数创建一个Thread类对象,并启动线程。
Callable创建线程比继承、实现Runnable更强?
- Callable的run方法可以拥有返回值,在线程结束后可以通过
FutureTask
对象获取返回值。 - Callable的run方法可以抛出异常,更加容易调试程序。
- Callable执行泛型,可以自定义返回值类型。
1.11、线程池
线程经常创建和销毁,如果使用特别大的资源,对性能就会有很大的影响,所以需要提前创建一个线程池,提前创建多个线程,当使用的时候就从线程池中获取,使用完后就将线程重新放入线程池中。
线程池好处:
- 提高响应速度,减少了创建和销毁线程的时间。
- 便于管理线程。
// 使用线程池来创建线程public class ThreadPool { public static void main(String[] args) { // 创建一个线程池,返回值是ExecutorService接口 ExecutorService service = Executors.newFixedThreadPool(10); // execute()执行一个实行Runnable接口实现类中的run方法 service.execute(new NumberA()); service.execute(new NumberA()); // submit()执行一个实现Callable接口实现类的中call方法 service.submit(new NumberB()); // 关闭线程池 service.shutdown(); }}class NumberA implements Runnable{ @Override public void run() { for (int i=0;i<100;i++){ if (i%2==0){ System.out.println(Thread.currentThread().getName()+" == "+i); } } }}class NumberB implements Callable<Object>{ @Override public Object call() throws Exception { for(int i=0;i<100;i++){ if (i%2!=0){ System.out.println(Thread.currentThread().getName()+" == "+i); } } return null; }}
ExecutorService
接口:真正的线程池接口,其常用子类ThreadPoolExecutor
// 这个方法返回值是ExecutorService,但是return是一个ExecutorService接口的实现类ThreadPoolExecutor,我们可以通过强制装换成ThreadPoolExecutor类,然后使用这个类中的属性和方法ExecutorService service = Executors.newFixedThreadPool(10);// ThreadPoolExecutor中重写的两个方法,一个是Executor抽象、一个是ExecutorService接口的方法,ExecutorService继承了Executor抽象类void execute(Runnable command); // Executor抽象方法,通常执行实现Runable接口的线程<T> Future<T> submit(Callable<T> task); // ExecutorService接口,通常执行实现Callable接口线程
Executors工具类:用于创建不同类型的线程池。
public static ExecutorService newFixedThreadPool(int nThreads); // 创建一个固定线程数的线程池public static ExecutorService newSingleThreadExecutor(); // 创建一个线程的线程池public static ExecutorService newCachedThreadPool(); // 创建一个可根据需要创建新线程的线程池public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize); // 创建一个可以根据调度延迟或定期执行的线程池
管理线程池:通过设置线程池属性,来管理线程池。
需要将ExecutorService
接口类型强制装换成ThreadPoolExecutor
类型类,才能设置对应的属性。
public static void main(String[] args) { // 创建一个线程池,返回值是ExecutorService接口 ExecutorService service = Executors.newFixedThreadPool(10); // 强转成ThreadPoolExecutor对象,设置属性来管理线程 ThreadPoolExecutor s = (ThreadPoolExecutor) service; s.setMaximumPoolSize(100); // 最大池大小。请注意,实际最大值在内部受CAPACITY限制。 s.setCorePoolSize(50); // 核心池大小是保持工作线程存活的最小数量 // execute()执行一个实行Runnable接口实现类中的run方法 s.execute(new NumberA()); s.execute(new NumberA()); // submit()执行一个实现Callable接口实现类的中call方法 s.submit(new NumberB()); System.out.println(Thread.currentThread().getName()+" === "+s.getClass()); // 关闭线程池 s.shutdown();}
创作打卡挑战赛 赢取流量/现金/CSDN周边激励大奖