线程的同步
问题的提出
卖票:现有三个窗口共同卖出100张票,模拟这个场景
package java2;
public class Window implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println("卖出第"+ticket+"张票");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else{
break;
}
}
}
}
package java2;
public class test {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
可能会出现错票,重票等现象
理想的状态

极端状态

多线程出现了安全问题
问题:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行
Synchronized
Java对于多线程的安全问题提供了专业的解决方式:同步机制
同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
package java2;
/*
使用同步代码块解决继承Thread类的方式的线程安全问题
例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
*/
public class Window implements Runnable{
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {
if (ticket > 0) {
System.out.println("卖出第" + ticket + "张票");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
} else {
break;
}
}
}
}
}
同步方法
package java2;
/*
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
*/
public class Window implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
fun();
}
}
public synchronized void fun(){
if (ticket > 0) {
System.out.println("卖出第" + ticket + "张票");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
同步的方式,解决了线程的安全问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性
同步机制中的锁
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
同步的范围
如何找问题,即代码是否存在线程安全?
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
切记
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能
释放锁的操作
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的**wait()**方法,当前线程暂停,并释放锁。
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
单例设计模式之懒汉式(线程安全)
package java2;
public class Singleton {
Singleton s;
private Singleton(){
}
public Singleton getS() {
if (s==null) {
synchronized (Singleton.class) {
if (s == null) {
s = new Singleton();
}
}
}
return s;
}
}
线程的死锁问题
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
package java2;
public class DeadLock {
public static void main(String[] args) {
String s1 = "111";
String s2 = "222";
MyThread myThread = new MyThread(s1,s2);
MyThread2 myThread2 = new MyThread2(s2,s1);
new Thread(myThread).start();
new Thread(myThread2).start();
}
}
class MyThread implements Runnable{
Object obj1;
Object obj2;
public MyThread(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1){
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1111");
synchronized (obj2){
System.out.println("2222");
}
}
}
}
class MyThread2 implements Runnable{
Object obj1;
Object obj2;
public MyThread2(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1){
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1111");
synchronized (obj2){
System.out.println("2222");
}
}
}
}
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
Lock(锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
package java2;
import java.util.concurrent.locks.ReentrantLock;
public class Window implements Runnable{
private int ticket = 100;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName()+"卖出第" + ticket + "张票");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
} else {
break;
}
lock.unlock();
}
}
}
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock --> 同步代码块(已经进入了方法体,分配了相应资源) --> 同步方法(在方法体之外)
线程的通信
使用两个线程打印 1-100。线程1,线程2 交替打印
package java2;
public class Test01 implements Runnable{
private int number = 1;
@Override
public void run() {
synchronized (this) {
while (true) {
if (number <= 100) {
this.notify();
System.out.println(Thread.currentThread().getName() + "打印" + number);
number++;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
package java2;
public class Tets01test {
public static void main(String[] args) {
Test01 t = new Test01();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
wait()与notify() 和 notifyAll()
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明
wait() 方法
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。
notify()/notifyAll()
- 在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
JDK5.0新增线程创建方式
实现Callable接口:
与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
package java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallTest implements Callable {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new CallTest());
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("qfdew");
}
@Override
public Object call() throws Exception {
return "1111";
}
}
使用线程池
**背景:**经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
**思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
package java2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//用线程池模拟死锁
public class DeadLock {
public static void main(String[] args) {
String s1 = "111";
String s2 = "222";
MyThread myThread = new MyThread(s1,s2);
MyThread2 myThread2 = new MyThread2(s2,s1);
ExecutorService ex = Executors.newFixedThreadPool(3);
ex.execute(myThread);
ex.execute(myThread2);
}
}
class MyThread implements Runnable{
Object obj1;
Object obj2;
public MyThread(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1){
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1111");
synchronized (obj2){
System.out.println("2222");
}
}
}
}
class MyThread2 implements Runnable{
Object obj1;
Object obj2;
public MyThread2(Object obj1,Object obj2){
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1){
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1111");
synchronized (obj2){
System.out.println("2222");
}
}
}
}
5232

被折叠的 条评论
为什么被折叠?



