五、线程同步(5.1两种实现 5.2一种实现)
5.1、 synchronized对象及方法
有的时候需要几行代码作为整体一起执行,实现线程同步,需要用到synchronized代码块
相关源码及其注释:
package day25;
public class Window implements Runnable {
private Integer no = 100;//表示票的编号 1~100
@Override
public void run() {
while(true) {
//this即Window对象,唯一的对象也被称之为锁,也可以写no,必须保证同一时刻只有一个线程拿到它
/* synchronized (this) {
if(no <= 0) {
break;
}
//必须让下面两条语句一块运行才能同步
System.out.println(Thread.currentThread().getName() + "销售第" + no + "张票");
no--;
}*/
if(no <= 0){
break;
}
sale();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized void sale(){
if(no <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "销售第" + no + "张票");
no--;
}
}
package day25;
/*
* 1、三个窗口销售同一堆票
* 2、每个编号的票只能被销售一次
* 3、票的编号的范围是1~100
*
* 线程同步机制:
* 1、synonronized代码块
* synchronized(对象) {
*
* }
* 1) synchronized代码块()中的对象必须是唯一的对象
* 2)唯一的对象 —— 锁,同一时刻只能有一个线程占据这个唯一的对象
* 3)哪个线程占据了这个对象,哪个线程就可以执行synchronized代码块中的代码,
* 此时其他线程就不能执行synchronized代码块中的代码,陷入阻塞状态
* 4)占据这个唯一的对象的线程执行完synchronized代码块后就会释放这个锁
* 2、synonronized方法 即Window里的sale方法
*
*
* */
public class MyTest {
public static void main(String[] args) {
Window w = new Window();
Thread window1 = new Thread(w);
Thread window2 = new Thread(w);
Thread window3 = new Thread(w);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
5.2、Lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
常用ReentrantLock来加锁实现锁
package day25;
import java.util.concurrent.locks.ReentrantLock;
public class Window implements Runnable {
private Integer no = 100;//表示票的编号 1~100
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
//this即Window对象,唯一的对象也被称之为锁,也可以写no,必须保证同一时刻只有一个线程拿到它
/* synchronized (this) {
if(no <= 0) {
break;
}
//必须让下面两条语句一块运行才能同步
System.out.println(Thread.currentThread().getName() + "销售第" + no + "张票");
no--;
}*/
/* if(no <= 0){
break;
}
sale();*/
try{
//加锁
lock.lock();
if(no <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "销售第" + no + "张票");
no--;
}finally {
lock.unlock();
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized void sale(){
if(no <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "销售第" + no + "张票");
no--;
}
}
5.3、线程安全的单例模式——懒汉式
package day25;
//懒汉式
public class SingleObject {
private static SingleObject obj;
private SingleObject() {
}
//加了synchronized关键字就是唯一和排他的了,整体运行完,这样就只会创建一次对象,单例
synchronized public static SingleObject getObject() {
if (obj == null) {
obj = new SingleObject();
}
return obj;
}
}
package day25;
public class MyTest2 {
private static SingleObject obj1;
private static SingleObject obj2;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
obj1 = SingleObject.getObject();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
obj2 = SingleObject.getObject();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(obj1 == obj2);
}
}
5.4、死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
相关源码现象展示,运行时会卡住:
package day27;
//发生嵌套同步了
public class A extends Thread{
@Override
public void run() {
while(true){
synchronized (Lock.m){
synchronized (Lock.n){
System.out.println("A............");
}
}
}
}
}
package day27;
public class B extends Thread{
@Override
public void run() {
while(true){
synchronized (Lock.n){
synchronized (Lock.m){
System.out.println("B............");
}
}
}
}
}
package day27;
public class Lock {
public static Object m = new Object();
public static Object n = new Object();
}
package day27;
public class MyTest {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.start();
b.start();
}
}
解决方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
六、线程通信
相关方法:
wait():令当前线程挂起并放弃CPU,同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待;
notifyAll():唤醒正在排队等待资源的所有线程结束等待。
以上三个方法只有在synchronized方法或代码块中才能使用。
6.1、打印数字:
package day28;
//1~20
/*
* wait()
* notify()
* notifyAll()
*
* */
public class PrintNum implements Runnable {
private Integer i = 1;
@Override
public void run() {
while (true) {
synchronized (Object.class){
if(i > 20){
break;
}
Object.class.notifyAll();//唤醒其他线程,通知别人我要睡了,wait方法
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
try {
Object.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
package day28;
public class MyTest {
public static void main(String[] args) {
PrintNum printNum = new PrintNum();
Thread th1 = new Thread(printNum);
Thread th2 = new Thread(printNum);
th1.setName("线程1");
th2.setName("线程2");
th1.start();
th2.start();
}
}
6.2、生产者消费者问题
此问题也称有限缓冲问题,是一个多线程同步问题的经典案例。
这里需要让生产者在缓冲区满时休眠(要么干脆放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,在唤醒消费者。
见源码及其注释:
package day28;
import java.util.ArrayList;
import java.util.Objects;
public class Store {
public static ArrayList<Object> list= new ArrayList<>(100);
public static final Integer MAX_NUM = 100;
}
package day28;
/*
* 生产者
* */
public class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Store.list) {
if (Store.list.size() < Store.MAX_NUM){
Store.list.notifyAll();
//生产
Object item = new Object();
Store.list.add(item);
System.out.println(Thread.currentThread().getName() + " - 生产商品,仓库目前数量:" + Store.list.size());
}else{
//停止生产
System.out.println(Thread.currentThread().getName() + " - 仓库已满,停止生产" );
try {
Store.list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
package day28;
/*
* 消费者
* */
public class Consumer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Store.list) {
if (Store.list.size() > 0){
Store.list.notifyAll();
//消费
Store.list.remove(0);
System.out.println(Thread.currentThread().getName() + " - 消费商品,仓库目前数量:" + Store.list.size());
}else{
//停止消费
System.out.println(Thread.currentThread().getName() + " - 仓库已空,停止消费" );
try {
Store.list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
package day28;
public class MyTest2 {
public static void main(String[] args) {
Producer p = new Producer();
Consumer c = new Consumer();
p.start();
c.start();
}
}
6.3、wait和sleep的区别
共同点:
wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
不同点:
方法归属不同:
sleep(long)是Thread的静态方法
而wait(),wait(long)都是Object的成员方法,每个对象都有
醒来时机不同:
执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去。
锁特性不同(重点)
wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃CPU,但你们还可以用)
而sleep方法如果在synchronized代码块中执行,并不会释放对象锁(我放弃CPU,但你们也不能用)
七、JDK5.0新增线程创建方式
7.1、实现Callable接口
与Runnable相比,Callable功能更强大些:
相比run()方法,可以有返回值;
方法可以抛出异常;
支持泛型的返回值;
需要借助FutureTask类,比如获取返回结果。
package day29;
import java.util.concurrent.Callable;
/*
* 实现Callable接口实现线程
* 1、实现Callable接口 - 泛型是返回值的类型
* 2、重写call方法
* */
public class SumThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Integer sum = 0;
for (int i = 0; i <=100 ; i++) {
sum+=i;
}
return sum;
}
}
package day29;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyTest {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>(sumThread);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
7.2、面试题:
Runnable和Callable的区别?
1)Runnable接口run方法没有返回值,Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果;
2)Callable接口支持返回执行结果,有返回值,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞;
3)Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。
7.2、线程池_创建线程,使用过程
池化技术:
线程池
连接池 - 网络连接
内存池
目的:减少对性能消耗比较大的操作(创建),提升系统的工作效率
如何解决:提前创建好,直接拿来用
相关源码:
package day30;
public class Num1Thread implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
package day30;
public class Num2Thread implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
package day30;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyTest1 {
public static void main(String[] args) {
//创建自定义线程类的对象
Num1Thread num1 = new Num1Thread();
Num2Thread num2 = new Num2Thread();
//创建有固定数量线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(100);
//通过线程池执行线程
pool.execute(num1);
pool.execute(num2);
//关闭线程池
pool.shutdown();
}
}
7.3、Collections_创建线程安全的集合