目录
- 背景
- 过程
- 历史
- 概念
- 实际应用
- 方法1:放方法名前形成同步方法;
- 方法2:使用同步块修改上面的例子;
- 应用方法
- 锁住对象:
- 锁住类:
- 总结
背景
学习并发,为解决并发带来的问题,引入synchronized 关键字。
过程
历史
当谈到synchronized同步锁的历史时,我们需要回顾Java语言的发展和多线程编程的背景。synchronized关键字是Java中用于实现线程安全的最早和最常用的机制之一。以下是synchronized同步锁的历史和演变:
早期Java版本(Java 1.0至Java 1.4):
Java的早期版本并没有提供很多高级的并发处理工具。在这些版本中,synchronized是实现多线程同步的主要手段。当一个方法或代码块被synchronized修饰时,它就变成了一个临界区(Critical Section),只允许一个线程在同一时间访问该方法或代码块。其他线程需要等待锁释放后才能进入这个临界区。
然而,早期的synchronized存在一些问题。首先,它的使用相对复杂,需要手动管理锁的获取和释放,容易出现死锁和竞态条件。其次,如果一个方法被声明为synchronized,即使没有并发问题,也会降低程序的性能,因为其他线程必须等待锁的释放。
Java 5引入的改进(Java 5,发布于2004年):
随着Java 5的发布,引入了java.util.concurrent包,其中包含更高级的并发工具,如ReentrantLock和Semaphore等。这些工具相对于synchronized提供了更多的灵活性和功能,允许开发人员更精细地控制多线程代码。
ReentrantLock是一个可重入锁,可以替代synchronized关键字,具有更丰富的特性。它支持公平性(Fairness),可中断(Interruptibility),以及尝试非阻塞地获取锁等特性。ReentrantLock的灵活性比synchronized更高,但使用起来也更加复杂。
Java 6至Java 7:
在Java 6和Java 7中,并发工具得到了一些改进和优化,但在同步锁机制方面没有太多变化。synchronized仍然是大多数Java程序员处理多线程问题时的首选方式。
Java 8的新特性(Java 8,发布于2014年):
随着Java 8的发布,引入了java.util.concurrent包中的新功能,如StampedLock和CompletableFuture等,这些功能进一步改进了并发编程的体验。尽管如此,synchronized仍然在许多场景中被广泛使用,因为它在简单情况下非常方便和易于使用。
Java 9及以后的版本:
在Java 9及以后的版本中,synchronized并没有被弃用或取代。它仍然是Java中最简单和直观的同步机制。然而,对于更复杂的并发问题,java.util.concurrent包中的高级工具仍然是更好的选择。
总结:
synchronized同步锁是Java早期用于实现线程安全的主要手段。虽然它相对简单,但在复杂的并发场景下可能不够灵活。随着Java版本的演进,java.util.concurrent包中引入了更高级的并发工具,使得处理多线程问题更加方便和安全。然而,对于简单的多线程情况,synchronized仍然是一个有效的选择。
概念
synchronized是Java中的一个关键字,用于实现多线程之间的同步。它主要用于保证多个线程在访问共享资源时的安全性,避免出现竞态条件(Race Condition)和数据不一致等并发问题。
当一个方法或代码块被synchronized修饰时,它就成为一个临界区(Critical Section)。在同一时间,只允许一个线程进入这个临界区执行代码,其他线程需要等待该临界区的释放。这样做的目的是确保在任意时刻,只有一个线程在执行被保护的代码,从而避免多个线程同时访问共享资源而导致的数据冲突和不一致。
synchronized关键字可以用于两种方式:
修饰实例方法:当synchronized修饰实例方法时,锁定的是当前实例对象(this)。只有获得了该实例对象的锁的线程才能执行该方法,其他线程必须等待。
Copy code
public synchronized void someMethod() {
// 临界区代码
}
修饰代码块:当synchronized修饰一个代码块时,需要指定一个对象作为锁。只有获得了该对象的锁的线程才能进入代码块执行,其他线程需要等待该对象锁的释放。
Copy code
public void someMethod() {
synchronized (lockObject) {
// 临界区代码
}
}
需要注意的是,synchronized虽然是最简单的同步机制,但它也有一些缺点。因为它是悲观锁(Pessimistic Lock),在竞争激烈的情况下,会导致其他线程长时间等待锁的释放,从而影响程序的性能。为了解决这个问题,Java引入了更高级的并发工具,如ReentrantLock等,以提供更多的灵活性和性能优化。然而,对于简单的多线程同步问题,synchronized仍然是一个有效的选择。
实际应用
java中cpu分给每个线程的时间片是随机的并且在java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。比如下面程序:
package com.pakage.ThreadAndRunnable;
public class Runnable_demo implements Runnable{
private int ticket=10;
public Runnable_demo(){
}
@Override
public void run() {
for(int i=0;i<20;i++){
if(this.ticket>0){
//休眠1s秒中,为了使效果更明显,否则可能出不了效果
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
}
}
}
public static void main(String args[]){
Runnable_demo demo=new Runnable_demo();
//基于火车票创建三个窗口
new Thread(demo,"a").start();
new Thread(demo,"b").start();
new Thread(demo,"c").start();
}
}
运行结果
我们可以看到c号窗口和和b号窗口都卖出了10号票,并且a号和b号窗口分别卖出了0号和-1号票。造成这种情况的原因是1、c线程和b线程在ticket=10的时候,c线程取出10号票以后,ticket还没来的及减1,b线程就取出了ticket此时ticket还等于10;2、在ticket=1时,c线程取出了1号票,ticket还没来的及减1,a、b线程就先后进入了if判断语句,这时ticket减1了,那么当a、b线程取票的时候就取到了0号和-1号票。
出现了上述情况怎样改变呢,我们可以这样做:当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后在把锁给另一个要用这个资源的线程。这样就不会出现上述情况。 实现这个锁的功能就需要用到synchronized这个关键字。
方法1:放方法名前形成同步方法;
package synchronizedLock;
public class Runnable_demo1 implements Runnable{
private int ticket=10;
public Runnable_demo1(){
}
@Override
public void run() {
for(int i=0;i<20;i++){
if(this.ticket>0){
//休眠1s秒中,为了使效果更明显,否则可能出不了效果
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
this.sale();
}
}
}
public synchronized void sale(){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
}
}
public static void main(String args[]){
Runnable_demo1 demo=new Runnable_demo1();
//基于火车票创建三个窗口
new Thread(demo,"a").start();
new Thread(demo,"b").start();
new Thread(demo,"c").start();
}
}
方法2:使用同步块修改上面的例子;
package synchronizedLock;
public class Runnable_demo2 implements Runnable{
private int ticket=10;
public Runnable_demo2(){
}
@Override
public void run() {
for(int i=0;i<20;i++){
if(this.ticket>0){
//休眠1s秒中,为了使效果更明显,否则可能出不了效果
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
this.sale();
}
}
}
public synchronized void sale(){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
}
}
public static void main(String args[]){
Runnable_demo2 demo=new Runnable_demo2();
//基于火车票创建三个窗口
new Thread(demo,"a").start();
new Thread(demo,"b").start();
new Thread(demo,"c").start();
}
}
应用方法
锁住对象:
package synchronizedLock.ObjectLock;
/**
* @BelongsProject: JAVAtest
* @BelongsPackage: synchronizedLock
* @Author: GuoYuan.Zhao
* @Description: 描述什么人干什么事儿
* @CreateTime: 2023-07-26 09:08
* @Version: 1.0
*/
public class TestSynchronized {
//1
public synchronized void minus() {
int count = 5;
for (int i = 0; i < 5; i++) {
count--;
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
第一种情况:
两个线程 thread1 和 thread2,同时访问对象的方法,由于该方法是 synchronized 关键字修饰的,那么这两个线程都需要获得该对象锁,一个获得后另一个线程必须等待。所以我们可以猜测运行结果应该是,一个线程执行完毕后,另一个线程才开始执行,运行例子,输出打印结果如下:
Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0
//2
// public synchronized void minus() {
// int count = 5;
// for (int i = 0; i < 5; i++) {
// count--;
// System.out.println(Thread.currentThread().getName() + " - " + count);
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// }
// }
// }
//
// public synchronized void minus2() {
// int count = 5;
// for (int i = 0; i < 5; i++) {
// count--;
// System.out.println(Thread.currentThread().getName() + " - " + count);
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// }
// }
// }
2、两个方法都加锁
两个线程调用
Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0
//3
// public synchronized void minus() {
// int count = 5;
// for (int i = 0; i < 5; i++) {
// count--;
// System.out.println(Thread.currentThread().getName() + " - " + count);
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// }
// }
// }
//
// public void minus2() {
// int count = 5;
// for (int i = 0; i < 5; i++) {
// count--;
// System.out.println(Thread.currentThread().getName() + " - " + count);
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// }
// }
// }
3、一个加锁一个不加
Thread-1 - 4
Thread-0 - 4
Thread-1 - 3
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-1 - 0
Thread-0 - 0
可以看到,结果是交替的,说明线程是交替执行的,说明如果某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。
}
package synchronizedLock.ObjectLock;
/**
* @BelongsProject: JAVAtest
* @BelongsPackage: synchronizedLock
* @Author: GuoYuan.Zhao
* @Description: 描述什么人干什么事儿
* @CreateTime: 2023-07-26 09:08
* @Version: 1.0
*/
public class Run {
public static void main(String[] args) {
//1
final TestSynchronized test = new TestSynchronized();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test.minus();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test.minus();
}
});
thread1.start();
thread2.start();
}
//2 3
// final TestSynchronized test = new TestSynchronized();
//
// Thread thread1 = new Thread(new Runnable() {
//
// @Override
// public void run() {
// test.minus();
// }
// });
//
// Thread thread2 = new Thread(new Runnable() {
//
// @Override
// public void run() {
// test.minus2();
// }
// });
//
// thread1.start();
// thread2.start();
// }
// }
}
锁住类:
package synchronizedLock.ClassLock;
/**
* @BelongsProject: JAVAtest
* @BelongsPackage: synchronizedLock.ClassLock
* @Author: GuoYuan.Zhao
* @Description: 描述什么人干什么事儿
* @CreateTime: 2023-07-26 09:36
* @Version: 1.0
*/
public class TestSynchronized {
//1
// public static synchronized void minus() {
// int count = 5;
// for (int i = 0; i < 5; i++) {
// count--;
// System.out.println(Thread.currentThread().getName() + " - " + count);
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// }
// }
// }
Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0
可以看到,类锁和对象锁其实是一样的,由于静态方法是类所有对象共用的,所以进行同步后,该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。
//2
public static synchronized void minus() {
int count = 5;
for (int i = 0; i < 5; i++) {
count--;
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
public synchronized void minus2() {
int count = 5;
for (int i = 0; i < 5; i++) {
count--;
System.out.println(Thread.currentThread().getName() + " - " + count);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
//Thread-1 - 4
Thread-0 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-1 - 0
Thread-0 - 0
可以看到两个线程是交替进行的,也就是说类锁和对象锁是不一样的锁,是互相独立的。
}
package synchronizedLock.ClassLock;
/**
* @BelongsProject: JAVAtest
* @BelongsPackage: synchronizedLock.ClassLock
* @Author: GuoYuan.Zhao
* @Description: 描述什么人干什么事儿
* @CreateTime: 2023-07-26 09:36
* @Version: 1.0
*/
public class Run {
public static void main(String[] args) {
//
// Thread thread1 = new Thread(new Runnable() {
//
// @Override
// public void run() {
// TestSynchronized.minus();
// }
// });
//
// Thread thread2 = new Thread(new Runnable() {
//
// @Override
// public void run() {
// TestSynchronized.minus();
// }
// });
//
// thread1.start();
// thread2.start();
//
//2
final TestSynchronized test = new TestSynchronized();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
TestSynchronized.minus();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test.minus2();
}
});
thread1.start();
thread2.start();
}
}
总结
synchronized 关键字主要有以下几种用法:
- 非静态方法的同步;
- 静态方法的同步;
- 代码块。
非静态方法的同步:
非静态方法的同步是针对实例对象的,即在同一时间内只允许一个线程访问该实例对象的同步方法。
静态方法的同步:
静态方法的同步是针对类的,即在同一时间内只允许一个线程访问该类的静态同步方法。
代码块:
synchronized关键字还可以用于代码块,这时候需要指定一个对象作为锁。在同一时间内,只允许一个线程进入该代码块执行。