Synchronized
Synchronized关键字回顾
synchronized是java中的关键字,是一种同步锁。它修饰的目标有以下几种:
- 1.修饰一个代码块,被修饰的代码块称为同步代码块,其作用的范围是大括号{},括起来的代码,作用的对象是调用这个代码块的对象,synchronized不能修饰静态代码块。
- 2.修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是所有调用这个方法的对象。
- 3.修饰一个静态方法,其作用范围是整个静态方法,作用的对象是这个类的所有对象。
- 4.修饰一个类对象(类的class对象),其作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。
作用的对象,有点不了解。以及synchronized锁作用在this对象,和作用在类.class上有什么区别?学完后面的课程,记得来回答这个问题。
关于synchronized的理解,共有两种类型的锁:
(1)类锁:只有synchronized修饰静态方法或者修饰一个类的class对象时,才是类锁。
(2)对象锁:除了类锁,所有其他的上锁方式都认为是对象锁。比如synchronized修饰普通方法或者synchronized(this)给代码块上锁等。
应该注意的是,因为一个类只有一个class对象,因此所有的访问者在访问被加了类锁的代码时,都是共用同一把锁,而类的实例却可以有很多个,因此不同对象访问加了对象锁的代码,它们的访问互不干扰。
synchronized锁的访问规则
(1)加了相同锁的代码,它们的访问规则是相同的,即当某个访问者获得该锁时,它们一起向该访问者开放访问,向其他没有获得该锁的访问者关闭访问。
(2)加了不同锁的代码访问互相不干扰。
(3)而没有加锁的代码随时都可以任意访问,不受任何限制。
判断是否同一把synchronized锁
(1)不同类型的锁不是同一把锁。
(2)加的是对象锁,那么必须是同一个对象实例才是同一把锁。
(3)加的是类锁,那必须是同一类才是同一把锁。
对于synchronized修饰的代码能否访问
1.首先判断是不是同一把锁。
2.然后判断各自的访问规则。
注意事项
虽然synchronized关键字可以来修饰方法,但synchronized关键字不属于方法定义的一部分。synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,子类又覆盖了该方法,在子类中的方法默认情况下是不进行同步的,而必须显示在子类的方法上也加上synchronized关键字才可以。当然,也可以在子类中直接调用父类方法,这样虽然子类方法不同步但是方法体的内容,是同步的,因此相当于子类方法也同步。
多线程编程步骤(上)
第一,创建资源类,创建属性和操作方法。第二 创建多线程调用资源类的方法。
售票案例
sale 销售 ticket 票
案例要求,3个售票员进行售票,共售卖30张票。
代码
/**
* @author 长名06
* @version 1.0
* 多线程编程步骤,第一步 创建资源类,定义属性和方法
* 第二步,创建多个线程,调用资源类的操作方法
*/
public class SaleTickets {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "aa").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "bb").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "cc").start();
}
}
class Ticket {
//票数
private static int number = 30;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t 卖出一张票"
+ "剩下票数为" + --number);
}
}
}
代码分析
-
1.synchronized关键字修饰的非静态方法,此时这个方法的锁就是synchronized(this)锁,就是对调用当前方法的对象上锁。案例中的aa,bb,cc线程都是使用ticket这同一个对象,调用的sale()方法,所以这三个线程是互斥的,同时只能由三个中的一个,使用sale()方法。从输出结果,也可以看出,是互斥访问的。
-
2.当一个线程获取了对应的锁,可以访问对应锁的代码块,其他需要访问该代码块的线程,就要等待。等待获取锁的线程释放锁,但是获取到锁的线程的执行有两种情况。
1)获取锁的线程,执行对应的代码块,然后线程释放锁,无事发生,正常情况;
2)拥有锁的线程执行中,出现了异常,此时JVM会让线程自动释放锁。 -
3.但如果获取锁的线程的操作,需要等待IO或者其他原因(如sleep方法)被阻塞了,但是线程没有释放锁,其他的线程就只能等待,会很影响执行效率。所以需要一种机制可以不让等待的线程一直无限的等待下去(等待一定的时间,就能响应中断),通过Lock(java.util.concurrent.locks包下的接口)就可以实现。
-
4.以上锁都是synchronized锁。
Lock
基本介绍
Lock锁实现,并提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。
Lock和synchronized的区别
- 1.Lock不是java语言内置的关键字,synchronized是Java语言的关键字,是内置特性。Lock是一个接口,可以通过其实现类实现异步访问。
- 2.采用synchronized关键字,不需要用户去手动释放锁,当synchronized关键字修饰的方法或代码块执行完之后,有JVM自动让线程释放锁。但是Lock则必须让用户手动释放锁,如果没有主动释放锁,就可能会出现死锁现象。
使用Lock实现sale_ticket案例
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 长名06
* @version 1.0
* 多线程编程步骤,第一步 创建资源类,定义属性和方法
* 第二步,创建多个线程,调用资源类的操作方法
*/
public class SaleTickets {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "aa").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "bb").start();
new Thread(() -> {
for (int i = 0; i < 40; i++){
ticket.sale();
}
}, "cc").start();
}
}
class Ticket {
//票数
private static int number = 30;
private final ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();//开启锁
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t 卖出一张票"
+ "剩下票数为" + --number);
}
}finally {
lock.unlock();//释放锁
}
}
}
关于Thread#start()&start0()
public synchronized void start() {//这个方法,完成线程的启动,但是实际创建线程是start0()方法完成的
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();//这里这个方法的调用,才是完成线程的创建
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();//此方法,java代码无法完成,而是用native关键字修饰的方法,就是使用jvm底层的JNI(Java Native Interface)机制调用c语言库完成的。这个创建线程的方法,以及是否创建,创建顺序,不是由java程序决定的,而是由程序运行的OS决定的。
lock接口方法
public interface Lock {
void lock();//获取锁
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();//释放锁
Condition newCondition();
}
lock方法
lock()方法使用最多的方法,功能,获取锁,如果锁被其他线程获取,则进行等待。
使用Lock,必须主动去释放锁,并且发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}finally{}块中进行,并且将释放锁的操作在finally块中,用来保证锁一定被释放,防止死锁的发生。通常使用Lock实现同步的,是以如下形式的。
lock.lock();//开启锁
try {
//...具体代码
}catch(Exception e){
}
finally {
lock.unlock();//释放锁
}
newCondition方法
关键字synchronized和wait()/notify()这两个方法一起使用,可实现等待/通知模式。Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知的模式。wait()和notify()是Object类下的方法,notify()被调用时,JVM会随机的唤醒某个等待的线程,使用Condition类可进行选择性通知,Condition常用的方法,awiat()方法,会使当前线程等待,同时会释放当前线程持有的锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。signal()用于唤醒等待的线程。
注意,在使用Condition接口(使用其具体的实现类)的await()和signa()方法前,需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在signal()调用后会从当前Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦获得锁成功,就执行。
小结
Lock和synchronized有以下几点不同
- 1.Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现的。
- 2.synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。Lock在发生异常时,如果没有主动通过unLock()会释放锁,则很可能会造成死锁现象,因此使用Lock时需要在finally块中释放锁。
- 3.Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一致等待,不能够使用中断。
- 4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- 5.Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果资源竞争不激烈,二者的性能是差不多的,而当资源竞争激烈时,此时Lock的性能要远远优于synchronized。