java语法基础 - 第九部分 - 线程同步

47 阅读7分钟

文章目录

1. 概念

同步:解决多线程访问同一资源,导致资源的不安全性 → 故引入线程同步

2. 线程运行状态图 – 重点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqJSq6rw-1574792097591)(en-resource://database/10575:1)]

2.1 线程终止 - 代码测试


简单示例 --线程终止 → ( 标志符,外部干涉 )

class Run4 implements Runnable {
	
	// 1. 当前线程停止的标志位
	boolean isStop = false;
	
	@Override
	public void run() {
		while(!isStop) {
			System.out.println(Thread.currentThread().getName() + ": 正在执行");
		}
	}
	
	// 2. 让线程停止
	public void stop() {
		isStop = true;
	}
	
}

// 测试代码
public static void main(String[] args) throws Exception {
    Run4 run4 = new Run4();

    Thread thread = new Thread(run4);

    thread.start();

    Thread.sleep(1000);
    int i = 0;
    while(true) {
        i++;
        if(i == 100) {
            run4.stop();
            System.out.println(thread.getName() + " :停止");
            break;
        }
    }
}

  运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqWQ1aj9-1574792097593)(en-resource://database/10579:1)]

2.2 线程阻塞join() - 代码测试

线程对象.join()当前线程一直占CPU, 其他线程阻塞, 直到当前线程执行完毕

  简单示例 - Thread.join()

class Thread1 extends Thread {	
	@Override
	public void  run() {
		for(int i = 0; i<10; i++) {
			System.out.println(Thread.currentThread().getName() + ".....正在运行");
		}
	}
}

// 代码测试
public static void main(String[] args) throws Exception {

    // 1. 开始main线程 开始创建 t1对象
    Thread t1 = new Thread1();

    // 2. main线程将  t1 线程放入就绪状态 -- 让 t1、main本身线程进行抢CPU
    t1.start();

    // 3. 当main线程抢到CPU时,main线程告诉CPU,把CPU让给t1线程进行使用,直到执行完毕为止,其他线程阻塞状态
    t1.join();
    
    // 4. 当t1线程执行完毕,该行的main线程的代码才执行
    for(int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + "...正在运行");
    }

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSxn6K90-1574792097604)(en-resource://database/10581:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OWHg7ogr-1574792097612)(en-resource://database/10583:1)]

2.3 当前线程抢到CPU,但强制其重新跟其他线程进行此次的竞争 - 代码测试 - yield()


代码示例 - Thread.yield()

class Thread2 extends Thread {	
	@Override
	public void  run() {
		for(int i = 0; i<1000; i++) {
			System.out.println(Thread.currentThread().getName() + ".....正在运行");		
		}
	}
}

// 测试代码
public static void main(String[] args) throws Exception {

    Thread t1 = new Thread2();

    t1.start();

    for(int i = 0; i<1000; i++) {
        if(i%5 == 0) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + i);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHjQoYs9-1574792097615)(en-resource://database/10587:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ft42HrrV-1574792097623)(en-resource://database/10589:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDYN2ssr-1574792097626)(en-resource://database/10591:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eYVnbURj-1574792097632)(en-resource://database/10593:1)]

2.4 线程等待, 不释放锁 – sleep()


简单示例 - Thread.sleep( long ) - 打印时间

public static void main(String[] args) throws Exception {
		
		// 1. 获取运行时那刻的时间
		Date date = new Date();
		long Millisecond = date.getTime();
		
		// 2. 打印时间
		while(true) {
			// 3. 获取下一秒的时间
			date = new Date();
			
			// 4. 20秒后自动停止main进程
			if(Millisecond + 20000 < date.getTime()) {
				break;
			}
			System.out.println(date);
			Thread.sleep(1000);
		}
}

2.5 线程等待, 释放锁 – wait()

3. Thread类、Runnable接口

1. 线程对象直接执行run()方法是不可以执行的,只能调用start(),由CPU调用run方法

2. Thread类:已经实现Runnable接口

3. Thread是静态代理

4. 静态代理特点:

  • 4.1 真实角色、代理角色
  • 4.2 两者实现相同的接口
  • 4.3 代理角色持有真实角色的引用

3.1 构造线程的形式

方法1 - 继承Thread

  1. 线程实现 - 1

缺点: 不可以在继承其他类

class ThreadDemo1 extends Thread {
    @Overrride
    public void run() {
         代码
    }
}

Thread t1 = new ThreadDemo1();


方法2 - 实现Runnable

  2. 线程实现 - 2(推荐这种)

优点

  • 1. 只实现接口,所以可以继承器其他类
  • 2. 方便共享资源,多个代理访问
  • 3. 静态代理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYRXaWu7-1574792097636)(en-resource://database/10569:1)]

class RunDemo1 implements Runnable {
    @Overrride
    public void run() {
         代码
    }
}

Thread t1 = new Thread( new RunDemo1() );


  模拟三个人( 代理角色 ) 抢 食物( 真实角色 )

class Foods implements Runnable {

	private int breadCount = 50;

	@Override
	public void run() {
		while (true) {
			if (breadCount < 0) {
				break;
			}
			System.out.println(Thread.currentThread().getName() + " 抢到了第 " + breadCount + " 面包");
			--breadCount;
		}
	}
}


//测试代码
public static void main(String[] args) {
    // 1. 真实角色
    Foods foods = new Foods();

    // 2. 三个代理角色,并且都持有同一个真实角色foods的引用
    Thread student1 = new Thread(foods, "student1");
    Thread student2 = new Thread(foods, "student2");
    Thread student3 = new Thread(foods, "student3");

    // 3. 三个学生开始抢面包
    student1.start();
    student2.start();
    student3.start();
}


方法3 - 实现Callable

优点:

  • 1. 可返回值、可声明异常


步骤:

  • 1. 实现Callable接口方法
  • 2. Executors.newFixedThreadPool( int ) – 获取ExecutorService对象( 创建线程池 )
  • 3. ExecutorService对象.submit( Callable实现类 ) – 获取Future对象 – 运行线程
  • 4. Future对象.get() – 线程运行时的返回值 – T类型
  • 5. ExecutorService对象.shutdownNow() – 停止线程池

class Thr3 implements Callable<String> {
	int id;
	public Thr3(int id) {
		this.id = id;
	}
    
	@Override
	public String call() throws Exception {
		return "Callable"+ id +"实现 --" + Thread.currentThread().getName();
	}
}

// 测试代码
public static void main(String[] args) throws Exception {

    // 1. 创建Callable对象
    Callable<String> thread1 = new Thr3(1);
    Callable<String> thread2 = new Thr3(2);

    // 2. 创建线程池
    ExecutorService es = Executors.newFixedThreadPool( 3 );

    // 3.  Callable对象的方法执行由线程池的某个线程来执行--  返回执行完线程后的结果对象
    Future<String> result = es.submit(thread1);
    Future<String> result2 = es.submit(thread2);

    // 4. 得到3步骤 获取线程执行完的结果
    String res = result.get();
    System.out.println(res);
    res = result2.get();
    System.out.println(res);

    // 5. 停止线程池中所有正在执行、等待的线程
    es.shutdownNow();
}

  运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EPVFgDmB-1574792097643)(en-resource://database/10571:1)]

3.2 同步(锁)

同步(线程安全): 解决 并发 导致的多线程访问同一资源,数据不同步

3.2.1 同步成员方法

当前线程抢到真实角色调用方法的使用,其他线程不可以使用同一真实角色的同步方法,直到当线程的同步方法运行完毕,其他线程才可以去抢同一真实角色的同步方法调用

  • 1. 线程同步 – 当前真实角色上锁 - 同一真实角色同一同步方法只能由一个线程进行调用
  • 2. 线程不同步 – 当前真实角色不上锁 – 同一真实角色非同步方法可由多个线程同时调用
class Foods2 implements Runnable {

	int count = 50;
	boolean isStop = false;

	@Override
	public void run() {
		while (!isStop) {
			// 1. 线程同步 -- 当前真实角色上锁 - 同一真实角色同一同步方法只能由一个线程进行调用
			gain1();

			// 2. 线程不同步 -- 当前真实角色不上锁 -- 同一真实角色非同步方法可由多个线程同时调用
//			gain2();  
		}
	}

	private synchronized void gain1() {
		
		// 1. 进来先查看面包是否还有,没有则立即停止所有线程的运行
		if (count < 1) {
			isStop = true;
			return;
		}
		
		// 2. 打印当前线程抢到的面包数
		System.out.println(Thread.currentThread().getName() + " : 获取第 " + count-- + " 个面包");
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void gain2() {
		if (count < 1) {
			isStop = true;
			return;
		}
		System.out.println(Thread.currentThread().getName() + " : 获取第 " + count-- + " 个面包");
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

//---------------------------
// 测试代码

// 1. 创建一个真实角色 --   一份资源
Runnable foods = new Foods2();

// 2. 创建三个线程 --  共同争抢 同一个真实角色
Thread student1 = new Thread(foods, "学生1");
Thread student2 = new Thread(foods, "学生2");
Thread student3 = new Thread(foods, "学生3");

// 3. 线程启动
student1.start();
student2.start();
student3.start();

  运行测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnjim2l4-1574792097645)(en-resource://database/10595:1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnRyY9pn-1574792097649)(en-resource://database/10597:1)]

3.2.1 同步代码块 – 方法内 – 锁对象

如果是 synchronized(this){ } 跟 同步成员方法 是一样的 – 都是锁当前对象


简单示例 - synchronized( this ) - 锁当前的真实对象

class Foods3 implements Runnable {

	int count = 50;
	boolean isStop = false;

	@Override
	public void run() {
		while (!isStop) {
			gain1();
		}
	}

	private void gain1() {
		
		synchronized (this) {
			// 1. 进来先查看面包是否还有,没有则立即停止所有线程的运行
			if (count < 1) {
				isStop = true;
				return;
			}
			// 2. 打印当前线程抢到的面包数
			System.out.println(Thread.currentThread().getName() + " : 获取第 " + count-- + " 个面包");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}


简单示例 - synchronized( this ) - 锁当前的真实对象

3.3 死锁 - 过多的同步

线程可能需要两个资源锁定才能运行完,然后只占有一个,另一个被另一个线程占用,互相等待,资源互相释放不了

  Thread3线程类 – 完成执行完run方法需要先抢到o2对象,然后在抢到o1对象才能执行完

class Thread3 extends  Thread {
	
	Object o1 ;
	Object o2;
	
	public Thread3(Object o1, Object o2) {
		super();
		this.o1 = o1;
		this.o2 = o2;
	}

	@Override
	public void run() {
		synchronized(o1) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized(o2) {
				System.out.println(Thread.currentThread().getName() + ":执行完成");
			}
		}
	}
	
}

  Thread4线程类 – 完成执行完run方法需要先抢到o1对象,然后在抢到o2对象才能执行完

class Thread3 extends  Thread {
	
	Object o1 ;
	Object o2;
	
	public Thread3(Object o1, Object o2) {
		super();
		this.o1 = o1;
		this.o2 = o2;
	}

	@Override
	public void run() {
		synchronized(o1) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized(o2) {
				System.out.println(Thread.currentThread().getName() + ":执行完成");
			}
		}
	}
	
}


利用上述两个线程类,进行死锁情况的制作

Object o1 = new Object();
Object o2 = new Object();

Thread thread1 = new Thread3(o1, o2);
Thread thread2 = new Thread4(o1, o2);

thread1.start();
thread2.start();

运行结果 – 没死锁多运行几遍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpcqTufv-1574792097659)(en-resource://database/10709:1)]

3.4 生产者、消费者 - 减少多锁导致死锁的机率

Producer-consumer problem 或者 Bounded-Buffer problem

思想:

    1. 固定的大小缓冲区 — 线程共享
    1. 生产者往缓冲区添加数据,消费者往缓冲区取走数据
    1. 缓冲区满时,生产者休眠 – 等待下次消费者取走数据时才唤醒
    1. 缓冲区空时,消费者休眠 – 等待下次生产者添加数据时才唤醒
3.4.0 模拟生产者、消费者遇到的问题

即消费者明知缓冲区空依然还在取数据

  共享缓冲区类

// 消费者、生产者共同分享访问的资源
class Movices {
	
	// 1. 生产者、消费者共同的缓存区
	String[] movies = new String[10];
	
	// 2. movies的位置
	int index = 0;
	
	// 3. 生产者生产的第几个片
	static int addCount = 1;
	
	// 4. 消费者已经看了第几个片
	static int watchCount = 1;
	public synchronized void play(String pic) {
		
		// 超出数组缓冲区,直接结束方法
		if(index >= 10) {
			System.out.println("生产者别存片了,我已经放不下啦");
			return;
		}
		
		movies[index] = pic;
		System.out.println(Thread.currentThread().getName() + ": 增加" + movies[index] + " -  第" + addCount++ + "部");
		++index;
		
	}
	public synchronized void watch() {
		
		// 数组一旦存满index=10,防止数组超出数组长度
		if(index >= 10) {
			index = 9;
		}
		System.out.println(Thread.currentThread().getName() + ": 观看" + movies[index] + " -  第" + watchCount++ + "部");
		if(index <= 0) {
			System.out.println("消费者你都没片看了,你到底在看什么啊!--------------------------------");
			return;
		}
		--index;
	}
	
}

生产者 – 将电影放入播放数组中

class Player implements Runnable {
	Movices mov;
	Player(Movices mov) {
		this.mov = mov;
	}
	
	@Override
	public void run() {
		// 一共生产15张电影
		for(int i = 1; i <= 15; i++) {
			mov.play("电影");
		}
	}
}

消费者 – 观看电影的人

class Watcher implements Runnable {
	
	Movices mov;
	
	Watcher(Movices mov) { this.mov = mov; }
	
	@Override
	public void run() {
		// 消费者即将观看15张电影
		for(int i = 1; i<= 15; i++) {
			mov.watch();
		}
	}
}

测试代码

Movices mov = new Movices();
new Thread( new Player(mov), "生产者").start();
new Thread( new Watcher(mov), "消费者").start();



运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhioXLIM-1574792097660)(en-resource://database/10713:0)]

3.4.1 信号灯法 - 标志位 - synchronized、wait()释放锁、notifyAll()唤醒线程

  修改一下共享缓冲区域的代码

class Movices {
	
	/*
	 * 1. true:生产者生产,消费者等待
	 * 2. false: 生产者等待,消费者消费
	 */
	boolean flag = true;
	
	// 1. 生产者、消费者共同的缓存区
	String[] movies = new String[10];
	
	// 2. movies的位置
	int index = -1;
	
	// 3. 生产者生产的第几个片
	static int addCount = 1;
	
	// 4. 消费者已经看了第几个片
	static int watchCount = 1;
	
	public synchronized void play(String pic) {
		 
		// flag=false则让生产线程此时此刻等待不运行,释放锁,让消费线程使用释放的锁
		if(!flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		// 生产一部电影
		++index;
		movies[index] = pic;
		System.out.println(Thread.currentThread().getName() + ": 增加" + movies[index] + " -  第" + addCount++ + "部");
		
		// 变为false,提醒消费者进行消费
		flag = false;
		this.notifyAll();
	}
	public synchronized void watch() {
		
		// flag=true则让消费线程此时此刻等待不运行,释放锁,让生产线程使用释放的锁
		if(flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		// 消费一部电影
		System.out.println(Thread.currentThread().getName() + ": 观看" + movies[index] + " -  第" + watchCount++ + "部");
		if(index < 0) {
			System.out.println("消费者你都没片看了,你到底在看什么啊!--------------------------------");
			return;
		}
		--index;
		
		// 提醒生产线程进行生产
		flag = true;
		this.notifyAll();
	}
	
}


运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f1yFjRtB-1574792097668)(en-resource://database/10715:0)]

3. Timer工具类、TimerTask抽象类 – 线程执行次数

注意这个工具类已经被 java.lang.concurrent 包下的类取代了,尽量少用Timer、TimerTask
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yOb0jlNL-1574792097669)(en-resource://database/10717:0)]

Timer的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tt1CgE4l-1574792097674)(en-resource://database/10719:0)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qwbRUtg-1574792097679)(en-resource://database/10723:0)]

  代码示例 – Timer.schedule( TimeTask, Date )

class Task extends TimerTask {
	
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "响了!:  该做作业了");
	}
	
}
        
//测试代码
 public static void main(String[] args) throws Exception {

		// 1. 声明闹钟 -- 并设置TimerTask线程的名字
		Timer timer = new Timer("闹钟");
		
		// 2.声明闹钟响后所做的事情
		Task task = new Task(); 
		
		// 3. 设置2秒后的时间点
		Date date = new Date( new Date().getTime() + 1000);
		
		// 4. 运行闹钟
		timer.schedule(task, date, 2);
}

  运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sUwJiFYM-1574792097685)(en-resource://database/10725:0)]