从新开始学Java JavaSE基础day21(线程安全、死锁、线程通信、线程池)

2021/7/3 12:21:38

本文主要是介绍从新开始学Java JavaSE基础day21(线程安全、死锁、线程通信、线程池),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、线程同步

1.1 概念

在多个线程同时执行,如果没有关系,则互相不影响无需考虑线程的安全以及同步,但是如果多个线程执行使用相同的变量或其他数据,就可能由于不同线程的运行导致数据不同步,从而产生线程安全问题

线程安全:多个线程同时执行,在同一时间对数据进行操作,可能导致数据的不准确

Java中提供了同步机制 (synchronized)来解决。

1.2 线程同步方式

1.2.1 同步代码块

使用synchronized关键字将代码块中的代码进行同步,当多个线程执行相同代码时,会进行阻塞(等待其他线程执行结束后再执行)

  • 格式

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

    同步锁

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

    注意

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

    • 锁对象 可以是任意类型。
    • 多个线程对象 要使用同一把锁。
public class TicketTest {
	public static void main(String[] args) {
		Runnable r = new Runnable() {
			private int ticket = 5;// 票数
			private Object o=new Object();
			@Override
			public void run() {
				while (ticket > 0) {
					synchronized (o) {
						if (ticket > 0) {
							try {
								Thread.sleep(1000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							String name = Thread.currentThread().getName();
							System.out.println(name + "卖出了第" + ticket-- + "票");
						}
					}
				}
				System.out.println("票已售完");
			}
		};

		// 创建窗口线程
		Thread t1 = new Thread(r, "窗口1");
		Thread t2 = new Thread(r, "窗口2");

		t1.start();
		t2.start();
	}
}

1.2.2 同步方法

使用synchronized关键字修饰方法,当多个线程调用该方法时,同一时间只允许一个线程执行

  • 格式

    public synchronized void method(){
        可能会产生线程安全问题的代码 
    }
    
  • 同步方法中的锁是谁?

    • 对于非static方法,同步锁就是this。
    • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
    public class TicketTest {
    	public static void main(String[] args) {
    		Runnable r = new Runnable() {
    			private int ticket = 5;// 票数
    			private Object o=new Object();
    			@Override
    			public void run() {
    				while (ticket > 0) {
    						get();
    				}
    				System.out.println("票已售完");
    			}
    			
    			public synchronized void get(){
    				if (ticket > 0) {
    					try {
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					String name = Thread.currentThread().getName();
    					System.out.println(name + "卖出了第" + ticket-- + "票");
    				}
    			}
    			
    		};
    
    		// 创建窗口线程
    		Thread t1 = new Thread(r, "窗口1");
    		Thread t2 = new Thread(r, "窗口2");
    
    		t1.start();
    		t2.start();
    	}
    }
    

1.2.3 Lock锁

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

    方法名说明
    public void lock()加同步锁。
    public void unlock()释放同步锁。

使用方式与使用同步代码块使用Object作为锁对象类似,在对应任务类创建锁对象,通过加锁与释放锁方法对中间执行的代码进行能同步

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketTest {
	public static void main(String[] args) {
		Runnable r = new Runnable() {
			private int ticket = 5;// 票数
			private Object o=new Object();
			private Lock l=new ReentrantLock();//可重入锁
			
			
			@Override
			public void run() {
				while (ticket > 0) {
					 
						//加锁
						l.lock();
						if (ticket > 0) {
							try {
								Thread.sleep(1000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							String name = Thread.currentThread().getName();
							System.out.println(name + "卖出了第" + ticket-- + "票");
						}
						//释放锁
						l.unlock();
					 
				}
				System.out.println("票已售完");
			}
		};

		// 创建窗口线程
		Thread t1 = new Thread(r, "窗口1");
		Thread t2 = new Thread(r, "窗口2");

		t1.start();
		t2.start();
	}
}

二、死锁

2.1 概念

死锁是由于线程同步代码书写不规范产生的多线程的错误之一

指两个线程或多个线程相互持有对方所需要的资源,导致线程都处于等待状态,无法往下执行,这就是死锁!

public class DieLockTest {
	public static void main(String[] args) {
		Game game=new Game();
		Report report=new Report();
		FatherThread father=new FatherThread(game, report);
		SonThread son=new SonThread(game, report);
		father.start();
		son.start();
	}
}

//父亲线程 执行拥有游戏资源 需要成绩资源
//儿子线程  执行拥有成绩资源  需要游戏资源

class Game{//游戏类
}
class Report{//成绩类
}
class FatherThread extends Thread{//父亲线程类
	private Game game;
	private Report report;
	public FatherThread(Game game, Report report) {
		super();
		this.game = game;
		this.report = report;
	}
	@Override
		public void run() {
			synchronized (game) {//获取游戏资源
				System.out.println("你把成绩单给我");
				synchronized (report) {//获取成绩资源
					System.out.println("我把游戏给你");
				}
			}
		}
}
class SonThread extends Thread{//儿子线程类
	private Game game;
	private Report report;
	public SonThread(Game game, Report report) {
		super();
		this.game = game;
		this.report = report;
	}
	@Override
		public void run() {
			 synchronized (report) {
				System.out.println("你把游戏给我");
				
				synchronized (game) {
					System.out.println("我把成绩单给你");
				}
				
			}
		}
}

三、线程池

3.1 概念

概念:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

3.2 使用线程池的优点

合理利用线程池能够带来三个好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

3.3 线程池的创建

3.3.1 线程池的主要参数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。

5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

5、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。

3.3.2 线程池使用

在java中提供了很多对应的线程创建方法,已经将对相应的参数填入,可以直接使用

1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

4、newScheduledThreadPool:适用于执行延时或者周期性任务。

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {
	public static void main(String[] args) {
		//使用线程池创建线程执行任务步骤
		//1、创建runnable任务对象
		Runnable r=new Runnable() {
			@Override
			public void run() {
				String name = Thread.currentThread().getName();
			
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(name+"执行任务结束");
			}
		};
		
		//2、根据需求创建对应线程池对象
		ExecutorService es=Executors.newFixedThreadPool(3);
		//在创建线程池对象后会自动创建指定个数的线程对象
		//当执行任务时,线程池会自动分配线程对象进行执行
		//执行完毕后自动回收
		es.submit(r);
		es.submit(r);
		es.submit(r);
		es.submit(r);
	}
}

四、线程通信

4.1 概念

线程通信是在线程同步的基础上,在多个线程处理相同资源时,使用不同的方式进行处理

本质:就是在线程同步的基础上调用方法使线程按照指定的顺序执行

4.2 线程等待与唤醒

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

4.2.1 等待唤醒实例

一个线程执行等待另一个线程唤醒

package com.yunhe.day0702;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class WaitTest {
	public static void main(String[] args) throws InterruptedException {
		Lock l=new ReentrantLock();
		WaitThread1 wt1=new WaitThread1(l);
		WaitThread2 wt2=new WaitThread2(l);
		wt1.start();
		Thread.sleep(5000);
		wt2.start();

	}
}


class WaitThread1 extends Thread{
	Lock l;
	
	public WaitThread1(Lock l) {
		super();
		this.l = l;
	}

	@Override
	public void run() {
		synchronized (l) {
			 for (int i = 1; i <=10; i++) {
				 try {
					Thread.sleep(500);
				} catch (InterruptedException e1) {

					e1.printStackTrace();
				}
				if(i==5){
					
					try {
						l.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}else{
					System.out.println(this.getName()+"=>"+i);
				}
			}
		}
	}
	
}
class WaitThread2 extends Thread{
	Lock l;
	
	public WaitThread2(Lock l) {
		super();
		this.l = l;
	}

	@Override
	public void run() {
		synchronized (l) {
			 l.notify();
		}
	}
	
}

两个线程交替等待唤醒

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class WaitTest {
	public static void main(String[] args) throws InterruptedException {
		Lock l=new ReentrantLock();
		WaitThread wt1=new WaitThread(l);
		WaitThread wt2=new WaitThread(l);
		wt1.start();
		wt2.start();
	}
}


class WaitThread extends Thread{
	Lock l;
	public WaitThread(Lock l) {
		super();
		this.l = l;
	}

	@Override
	public void run() {
		 synchronized (l) {
			 for (int i = 1; i <=10; i++) {
				 l.notifyAll();
				 try {
					Thread.sleep(500);
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
                 System.out.println(this.getName()+"=>"+i);
				if(i==5){
					try {
						l.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} 
			}
		}
	}
}
  • waitnotify方法需要注意的细节
    • wait方法与notify方法必须要由同一个锁对象调用

      因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

    • wait方法与notify方法是属于Object类的方法的

      因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

    • wait方法与notify方法必须要在同步代码块或者是同步函数中使用

      因为:必须要通过锁对象调用这2个方法

wait方法会使当前正在使用锁对象的线程进入阻塞状态,notify会唤醒其他线程进行就绪态

4.3 wait等待方法与notify唤醒方法执行流程

在这里插入图片描述

对于拥有同步方法的线程而言,线程的执行状态改变不止因为计算机分配资源,还需要执行的锁对象才会从可执行态进入执行态。

wait方法将当前使用锁对象的线程进入阻塞状态,等待其他同步线程在使用锁对象调用notify方法进行唤醒

(调用wait方法进入阻塞态的线程只有使用相同的锁对象调用notify方法才能唤醒),使之在获取锁对象之后进入可运行态由系统分配资源进入运行状态。

五、volatile关键字

volatile保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:

  • 线程对变量进行修改之后,要立刻回写到主内存。
  • 线程对变量读取的时候,要从主内存中读,而不是缓存

各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率。

volatile是不错的机制,但是volatile不能保证原子性。

注意:即使没有书写volatile关键字,系统在资源充足的情况下也会尽量保证变量数据在各个线程之间的可见性与准确性

/**
 * volatile用于保证数据的同步,也就是可见性
 */
public class VolatileTest {
	private volatile static int num = 0;
	public static void main(String[] args) throws InterruptedException {
		new Thread(
            new Runnable(){
                public void run(){
                    while(num==0) { //此处不要编写代码

                    }
                }
            }
		) .start();
		
		Thread.sleep(1000);
		num = 1;
	}
}

六、设计模式

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

由前人不断的开发书写总结出来,用于实现某些特定的功能的最简洁完善的代码

在java中设计模式一共分为三大类共23种
在这里插入图片描述

6.1 单例模式

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。

唯一无参构造方法使用private修饰,提供静态的getInstance方法创建唯一对象并返回

6.1.1 懒汉单例模式

在类加载时不进行唯一对象的创建,而是在第一次使用时进行对象的创建。

//懒汉单例模式
class LazySingleton{
	//属性静态私有
	private static LazySingleton lazySingleton;//用来保存当前类唯一对象
	private LazySingleton(){}//唯一无参构造方法私有
	//方法同步静态
	public synchronized  static LazySingleton getInstance(){
		//懒汉模式
		//只有第一次调用方法时判断是否存在不存在创建
		if(lazySingleton==null){
			lazySingleton=new LazySingleton();
		}
		return lazySingleton;
	}
}

6.1.2 饿汉单例模式

在类加载时直接创建对象

//饿汉单例模式
class HungrySingleton{
	//属性静态私有
	private static HungrySingleton hungrySingleton=new HungrySingleton();//用来保存当前类唯一对象
	//饿汉会直接在属性默认值直接创建唯一对象
	private HungrySingleton(){}//构造方法私有
	
	public static HungrySingleton getInstance(){
		return hungrySingleton;
	}
	//因为在声明属性时直接创建,所以无需考虑线程安全问题
}

6.1.3 双重校验锁懒汉单例模式

在同步代码块执行之前与执行时进行双重判断。减少重复创建的可能

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleto=n;  
    }  

6.1.4 私有内部类饿汉单例模式

书写私有内部类,通过外部类方法进行内部类属性创建,提高属性安全性

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

6.2 工厂模式

将类的创建交由工厂类完成,我们只需要通过对应方法获取对应对象即可

6.2.1 简单工厂模式

创建工厂类,将工厂类可以创建的类书写在工厂类中,提供方法获取对应对象

// 抽象产品类
interface class Car {
    public void run();
    public void stop();
}
 
// 具体实现类
class Benz implements Car {
    public void run() {
        System.out.println("Benz开始启动了。。。。。");
    }
 
    public void stop() {
        System.out.println("Benz停车了。。。。。");
    }
}
 
class Ford implements Car {
    public void run() {
        System.out.println("Ford开始启动了。。。");
    }
 
    public void stop() {
        System.out.println("Ford停车了。。。。");
    }
}
 
// 工厂类
class Factory {
    public static Car getCarInstance(String type) {
        Car c = null;
        if ("Benz".equals(type)) {
            c = new Benz();
        }
        if ("Ford".equals(type)) {
            c = new Ford();
        }
        return c;
    }
}
 
public class Test {
 
    public static void main(String[] args) {
        Car c = Factory.getCarInstance("Benz");
        if (c != null) {
            c.run();
            c.stop();
        } else {
            System.out.println("造不了这种汽车。。。");
        }
 
    }
 
}

面试题

wait、sleep的区别

wait与sleep都可以使线程由运行态转化为阻塞态

wait由Object类提供所有的锁对象都可以使用,sleep由Thread类提供

wait方法会让当前获取对象锁的线程进入阻塞状态(必须获取锁对象)

sleep方法就是让当前线程进入阻塞状态

wait方法必须使用notify进行唤醒获取锁对象资源后继续执行

sleep方法在休眠时间到达后自动进入就绪态

wait方法执行后当前线程会释放锁对象

sleep方法不会释放锁对象

练习

使用线程通信完成,线程1执行1~5进入阻塞 线程2执行1~10 之后唤醒线程1继续执行

Thread-1 =>1 2 3 4 5

Thread-2 =>1 2 3 4 5 6 7 8 9 10

Thread-1 =>6 7 8 9 10

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
 * **使用线程通信完成,线程1执行1~5进入阻塞 线程2执行1~10 之后唤醒线程1继续执行**

	Thread-1  =>1 2 3 4 5
	
	Thread-2  =>1 2 3 4 5 6 7 8 9 10
	
	Thread-1  =>6 7 8 9 10
 */
public class HomeWork1 {
public static void main(String[] args) throws InterruptedException {
		Lock l = new ReentrantLock();
		WaitThread1 wt1 = new WaitThread1(l);
		WaitThread2 wt2 = new WaitThread2(l);
		wt1.start();
		Thread.sleep(5000);
		wt2.start();
		
	}
}
class WaitThread1 extends Thread{
	Lock l;

	public WaitThread1(Lock l) {
		super();
		this.l = l;
	}

	@Override
	public void run() {
		synchronized(l){
			for(int i = 1;i<=10;i++){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				if(i==5){
					System.out.println(this.getName()+"=>"+i);
					try {
						l.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				else{
					System.out.println(this.getName()+"=>"+i);
				}
			}
		}
	}
	
}
class WaitThread2 extends Thread{
	Lock l;
	
	public WaitThread2(Lock l) {
		super();
		this.l = l;
	}

	@Override
	public void run() {
		synchronized (l) {
			 l.notify();
			 synchronized(l){
				 for(int i = 1;i<=10;i++){
						try {
							Thread.sleep(1000);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						if(i==10){
							System.out.println(this.getName()+"=>"+i);
								l.lock();
						}
						else{
							System.out.println(this.getName()+"=>"+i);
						}
						
					
				}
			 }
		}
	}
	
}



这篇关于从新开始学Java JavaSE基础day21(线程安全、死锁、线程通信、线程池)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程