文章目录
四、线程同步
为什么需要线程同步?
- 多个线程执行的不确定性引起执行结果的不稳定;
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
例子:创建三个窗口卖票,总票数为100张
存在线程的安全问题(错票,重票)
class windows1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
try {
Thread.sleep(100);//加了sleep之后,加大了出现票数0和-1的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else break;
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
var windows = new windows1();
var t1 = new Thread(windows);
var t2 = new Thread(windows);
var t3 = new Thread(windows);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
问题:卖票过程中程序出现了重票和错票的情况------>出现了线程的安全问题
原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票,,导致了重票和错票的问题。
解决:在一个线程在操作数据过程中,其他线程不能参与进来,只有操作完数据,其他线程才能参与进来。这种情况,即使这个线程出现了阻塞,也不能被改变。
Java当中,我们通过同步机制来解决线程的安全问题
同步的方式解决了线程的安全问题,但是操作同步代码块时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率相对低一些。
方法一:同步代码块
synchronized(同步监视器){
//需要被同步的代码,即操作共享数据的代码,即为需要被同步的代码
}
- 共享数据:多个线程共同操作的数据例如上面的ticket
- 同步监视器(俗称,锁):任何一个类的对象都可以充当锁。(注意:该对象不能为匿名的,且不能在包含同步代码块的方法中声明)
- 多个线程必须要共用同一把锁,即对象不能为匿名的,且不能在包含同步代码块的方法中声明,如果使用当前类的对象充当锁,需要用当前类名.class来充当锁,否则会出现栈溢出的异常
- 在继承Thread类这种线程的创建方式中,锁 的对象必须是静态的,即使用同一把锁
- 在实现Runnable接口创建线程的同步代码块中,锁可以用this来代替(是同一个对象),但是在继承Thread类这种线程的创建方式中,锁不能使用this(此时的this是三个不同的对象)
示例代码:
class windows1 implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
//注意匿名对象不可以
while (true) {
synchronized (obj) {//注意锁要放在循环体内部,否则线程一知占用CPU
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else break;
}
}
}
}
方法二:同步方法
如果操作共享数据的代码完整的声明在一个方法中。我们不妨将此方法声明为同步的。
实现Runnable接口的线程安全问题:
package JavaSE.Thread.exr;
/*
例子:创建三个窗口卖票,总票数为100张
存在线程的安全问题
方式二:同步方法:
解决实现Runnable接口的线程安全问题:
*/
class windows3 implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (ticket > 0){
show();
}
}
private synchronized void show(){//同步监视器就是this
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class Window_Test3 {
public static void main(String[] args) {
var windows = new windows3();
var t1 = new Thread(windows);
var t2 = new Thread(windows);
var t3 = new Thread(windows);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
使用同步方法来处理继承Thread类的线程安全问题:
同步方法必须是静态的
package JavaSE.Thread.exr;
/*
使用同步方法来处理继承Thread类的线程安全问题
*/
class windows4 extends Thread {
private static int ticket = 100;//static 静态变量可以让三个线程的总票数为100张,不加static总票数为300张。
private static Object obj = new Object();
@Override
public void run() {
//不是同一把锁,出现了线程的安全问题
while (ticket > 0) {
show();
}
}
private static synchronized void show(){//同步监视器this,但是this对象有三个,所以方法必须是静态的
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class Window_Test4 {
public static void main(String[] args) {
var win1 = new windows4();
var win2 = new windows4();
var win3 = new windows4();
win1.start();
win2.start();
win3.start();
}
}
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明;
- 非静态的同步方法,同步监视器是:this(参考上面解决实现Runnable接口的线程安全问题)
- 静态的同步方法,同步监视器是:当前类本身(参考使用同步方法来处理继承Thread类的线程安全问题)
线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
死锁的例子:
package JavaSE.Thread;
/**
* 演示线程的死锁问题
*
*/
public class Thread_Test4 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
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();
new Thread(new Runnable() {
@Override
public void run() {
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();
}
}
死锁的解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量减免嵌套同步
方法三:jdk5新增 Lock锁
步骤:
- 实例化ReentrantLock
- 调用锁定方法lock()
- 调用解锁方法:unlock()
示例代码:
package JavaSE.Thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题之三:Lock锁
*/
class windows4 implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock(true);//true 代表公平的,先进先出 false代表随机的
@Override
public void run() {
//注意匿名对象不可以
while (true) {
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else break;
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class Lock_Test{
public static void main(String[] args) {
var windows = new windows4();
var t1 = new Thread(windows);
var t2 = new Thread(windows);
var t3 = new Thread(windows);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
synchronized(同步代码块,同步方法)与lock的异同?
- 相同点:二者都可以解决线程的安全问题
- 不同点sychronized机制在执行完相应的同步代码以后,自动的释放同步监视器。而lock需要手动启动同步(lock()),同时结束同步也需要手动的实现unlock()
五、线程通信问题
涉及到的三个方法
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
注意:
- wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中。Lock锁中都不可以使用。
- wait()、notify()、notifyAll()三个方法的调用者,必须是同步代码块或同步方法中的监视器发起的调用,否则会发生异常。
- wait()、notify()、notifyAll()三个方法是定义在java.lang.Object类中
使用举例:
package JavaSE.Thread;
class Number implements Runnable {
private static int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notifyAll();
if (number <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
} else break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
var num = new Number();
var t1 = new Thread(num);
var t2 = new Thread(num);
var t3 = new Thread(num);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
sleep()和wait()方法的异同
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态;
- 不同点:
- 两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块或同步方法中调用
- 关于释放同步监视器的问题:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
生产者,消费者问题:
生产者将产品交给店员,而消费者从店员处取走商品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位房产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能会出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到;
- 消费者比生产者快时,消费者会取相同的数据。
代码:
package JavaSE.Thread.exr;
class Clrek{
private static int num = 0;
public static int getNum() {
return num;
}
public static void setNum(int num) {
Clrek.num = num;
}
//生产产品
public synchronized void producerProduct(){
if (num < 20){
num++;
System.out.println(Thread.currentThread().getName() + "生产产品 当前产品数量" + this.getNum());
notify();//Object类的方法
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费产品
public synchronized void consumerProduct(){
if (num > 0){
num--;
System.out.println(Thread.currentThread().getName() + "消费产品 当前产品数量" + this.getNum());
notify();
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Producer extends Thread{
private Clrek clrek;
public Producer(Clrek clrek){
this.clrek = clrek;
}
@Override
public void run() {
while (true){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clrek.producerProduct();
}
}
}
class Consumer extends Thread{
private Clrek clrek;
public Consumer(Clrek clrek){
this.clrek = clrek;
}
@Override
public void run() {
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clrek.consumerProduct();
}
}
}
public class Product_Test {
public static void main(String[] args) {
Clrek clrek = new Clrek();
Producer p1 = new Producer(clrek);
p1.setName("生产者一:");
Consumer c1 = new Consumer(clrek);
c1.setName("消费者一:");
p1.start();
c1.start();
}
}
六、高级主题jdk5.0新增的线程创建方式
新的创建方式之一:实现Callable接口
与使用Runnable接口相比,Callable功能更加强大:
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助Future Task类,比如获取返回的结果
- Future接口
- 可以对具体Runnable/Callable任务的执行结果进行取消、查询是否完成、获取结果
- FutureTask是Future接口的唯一实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行、又可以作为Future得到Callable的返回值。
- Future接口
创建步骤:
- 创建一个实现Callable接口的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中。
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象,作为参数传递到FutureTask的构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
- 获取Callable接口中call()的返回值(可以不获取)
示例代码:
package JavaSE.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//创建一个实现Callable接口的实现类
class NumThread implements Callable{
//实现call方法,将此线程需要执行的操作声明在call()中。
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class Callable_Test {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
var numT = new NumThread();
//4.将此Callable接口实现类的对象,作为参数传递到FutureTask的构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numT);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
new Thread(futureTask).start();
try {
//6.获取Callable接口中call()的返回值
//返回值为构造器参数Callable实现类重写的call()的返回值。
Object num = futureTask.get();
System.out.println(num);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
新的创建方式之二:使用线程池
- **背景:**经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- **思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁的吹昂见销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- **corePoolSize:**核心池的大小
- **maximumPoolSize:**最大线程数
- **keepAliveTime:**线程没有任务时最多保持多长时间后会终止
线程池相关API
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Exocutors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Futuresubmit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭线程池连接
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
package JavaSE.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPool_Test {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
var executorService = Executors.newFixedThreadPool(10);
//设置线程池的属性
var sevice = (ThreadPoolExecutor) executorService;
sevice.setCorePoolSize(1);
//适用于Runnable
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。
executorService.execute(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + "====" +i);
}
}
}
});
//适合使用于Callable
executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + "====" +i);
}
}
return null;
}
});
//3.关闭线程池
executorService.shutdown();
}
}
设置线程池的属性: