在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,如果使用多线程程序,就会发生两个线程抢占资源的问题,所以在多线程编程中,需要防止这些资源访问的冲突,Java提供线程同步的机制来防止资源访问的冲突。
如何理解同步机制?
举一个生活中的例子,比如一些景点,表演婚俗中抛绣球活动,这个绣球被抛出去以后,下面所有的游客都会去争抢这唯一的绣球,谁抢到绣球就是这个幸运的人,迎娶新娘。
我们能够发现,无论多少人在下面等待抢绣球,但最终,只有一个人能拿到这个绣球,成为唯一的幸运儿。在这个例子中,就是一个典型的同步机制。
1.同步块
在Java中提供了同步机制,可以有效地防止资源冲突,同步机制使用synchronized关键字。
synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock(绣球),在多线程(游客)并发访问的时候,同时允许一个线程(游客)可以获得这个锁,执行特定的代码(迎娶新娘)。执行后释放锁,继续由其他线程争抢
代码演示:
public class SyncSample {
class Printer{
public void print(){
try {
Thread.sleep(500);
System.out.print("魑");
Thread.sleep(500);
System.out.print("魅");
Thread.sleep(500);
System.out.print("魍");
Thread.sleep(500);
System.out.print("魉");
System.out.println( );
}catch (Exception e){
e.printStackTrace();
}
}
在SyncSample外部类中创建内部类Printer,在print()方法体中,利用Thread.sleep(500)写入一个线程沉睡半秒输出的语句,使代码打印出有一个停顿的效果,作为sleep会抛出异常,我们用try—catch来捕获
再次增加内部类PrintTesk ,Runnable作为接口,在多线程编程中,Runnable
接口通常与线程结合使用,用于创建新的线程来执行特定的任务。
可以通过将Runnable
对象传递给Thread
类的构造函数来创建一个新的线程,并在该线程中执行Runnable
任务
class PrintTask implements Runnable{
public Printer printer; //定义类的成员变量
@Override
public void run() { //Runnable接口的核心方法run()
printer.print(); //调用printer对象的print方法,执行输出“魑魅魍魉”操作
}
}
再继续加入一个start方法,用于创建线程后实现输出
public void start() {
Printer printer = new Printer();//创建Printer类的实例
for (int i = 0; i < 10; i++) { //循环创建线程
PrintTask tack = new PrintTask(); //在每次循环中,创建一个PrintTask对象tack,并将之前创建的printer对象赋值给tack的printer成员变量
tack.printer = printer;
Thread thread = new Thread(tack); //创建一个新的Thread对象,将tack作为参数传入构造参数即这个线程会执行tack中的run方法
thread.start(); //开始线程执行
}
}
最后在主函数调用
public static void main(String[] args) {
SyncSample syncSample = new SyncSample();
syncSample.start();
}
运动结果
我们发现,再多线程程序运行的时候,十个线程底层都是引用了同一个printer对象, 这会使十个线程同时执行,同时对魑进行输出,然后又会对魅进行输出,接着魍魉进行一个个输出,最后得到以上无序混乱的结果。
在多线程情况下,所有线程都在争抢同一个资源,也就是当前printer对象时,使得我们打印出的数据并不是完整合适的,出现混乱的情况。
为了解决这样的问题,我们需要引入锁的机制,让线程一个个排队来执行,对于打印输出的代码,如果前面的线程没有执行完毕,后面所有的线程就一直处于阻塞等待的状态,直到拥有获取这个线程的执行权力
class Printer {
Object lock =new Object();
public void print() {
synchronized (lock) {
try {
Thread.sleep(500);
System.out.print("魑");
Thread.sleep(500);
System.out.print("魅");
Thread.sleep(500);
System.out.print("魍");
Thread.sleep(500);
System.out.print("魉");
System.out.println( );
} catch (Exception e) {
e.printStackTrace();
}
}
Object lock = new Object();
在Printer内部类中加入了一个用于同步的对象锁,这个对象可以被多个线程用来进行同步操作,确保在同一时间只有一个线程能够访问被synchronized
关键字修饰的代码块。
再次运行:
在程序运行时由Java来控制锁对象的分配,哪个线程优先的获取到了锁,哪个线程就可以执行里面的代码,这段代码执行完成后,锁会被释放,剩余的其他线程会争抢这把锁,之后按照相同的规则,抢夺—释放—抢夺,直到所有线程完成处理,程序结束。
关于synchronized的锁对象,synchronized用于实现线程同步的机制
- synchronized代码块 -任意对象即可
- synchronized方法 -this当前对象
- synchronized静态方法 - 该类的字节码对象
1.synchronized代码块 -任意对象即可
同以上案例代码一样,内部类中新增锁对象lock,在代码中为了保证线程的同步,使用synchronized()关键字,括号中增加锁对象lock,这样在多线程运行过程中,哪个线程先争抢到这个锁,它就拥有代码的执行权,其他线程处于阻塞状态
class Printer {
Object lock =new Object(); //定义锁对象
public void print() {
synchronized (lock) {
try {
...............
...............
} catch (Exception e) {
e.printStackTrace();
}
}
2.关于synchronized方法 -this当前对象
public synchronized void print2 () { //synchronized直接放在代码声明部分
try {
Thread.sleep(500);
System.out.print("魑");
Thread.sleep(500);
System.out.print("魅");
Thread.sleep(500);
System.out.print("魍");
Thread.sleep(500);
System.out.print("魉");
System.out.println( );
} catch (Exception e) {
e.printStackTrace();
}
此时synchronized不在代码中进行书写,而是把synchronized关键字放在声明部分 ,它是写在方法上进行描述,当一个方法被声明为synchronized时,多个线程在访问这个方法时会进行同步,既同一时间只有一个线程能够执行这个方法
synchronized方法使用的是对象锁,锁的对象是当前实例(this),表示一个线程进入一个sy实例方法后,它会获取当前对象的锁,其他线程如果想进入同一个对象这个synchronized方法,就必须等待当前线程释放这个锁
3.synchronized静态方法 - 该类的字节码对象
在Java中,当一个方法被声明为synchronized static(同步静态方法)时,它使用的锁是该类的字节码对象。
锁的对像
对于静态方法,锁的对象是类的class对象(该类的字节码对象),这意味着不同的对象实例在调用同一个类的synchronized static方法时,也会进行同步
public class SyncSample {
static class Printer { //添加静态方法
Object lock =new Object();
public void print1() {
synchronized (lock) {
try {
Thread.sleep(500);
System.out.print("魑");
Thread.sleep(500);
System.out.print("魅");
Thread.sleep(500);
System.out.print("魍");
Thread.sleep(500);
System.out.print("魉");
System.out.println( );
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static synchronized void print2 () {
try {
Thread.sleep(500);
System.out.print("魑");
Thread.sleep(500);
System.out.print("魅");
Thread.sleep(500);
System.out.print("魍");
Thread.sleep(500);
System.out.print("魉");
System.out.println( );
} catch (Exception e) {
e.printStackTrace();
}
与synchronized实例方法的区别:
-
锁的对象不同:
synchronized
实例方法锁的是当前对象实例(this
)。synchronized static
方法锁的是类的字节码对象。
-
同步范围不同:
- 对于实例方法,只有同一个对象实例的调用才会被同步。不同对象实例调用同一个实例方法是相互独立的,除非在方法内部有共享的状态。
- 对于静态方法,无论有多少个对象实例,只要是对同一个类的静态方法的调用,都会被同步。