synchronized 关键字

目录

  • 背景
  • 过程
    • 历史
    • 概念
    • 实际应用
      • 方法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();
	 }
	
}

运行结果
synchronized 关键字_第1张图片

我们可以看到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();
    }

}

synchronized 关键字_第2张图片

方法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();
    }

}

synchronized 关键字_第3张图片

应用方法

锁住对象:

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关键字还可以用于代码块,这时候需要指定一个对象作为锁。在同一时间内,只允许一个线程进入该代码块执行。

你可能感兴趣的:(并发问题,开发语言,java,并发,synchronized)