异步
public class PrintObject {
public void printString(){
System.out.println("begin");
if(Thread.currentThread().getName().equals("a")){
PrintStream printStream = System.out;
printStream.println("线程 a 运行");
}
if(Thread.currentThread().getName().equals("b")){
System.out.println("线程 b 运行");
}
System.out.println("end");
}
}
public static void main(String[] a) {
PrintObject pb = new PrintObject();
Thread thread1 = new Thread(pb::printString);
thread1.setName("a");
thread1.start();
Thread thread2 = new Thread(pb::printString);
thread2.setName("b");
thread2.start();
}
我们创建两个线程来调用同一业务对象的相同功能时, 可以看到下面输出.
begin
begin
线程 a 运行
end
线程 b 运行
end
两个线程在一起执行 printString
方法, 并且交叉打印. 也就是说当我们启动一个线程执行某个方法的时候就是异步执行, 至于为啥要这样演示, 是因为下面的同步.
同步
将 synchronized public void printString()
方法上加入 synchronized
(内置锁) 关键字, 来使方法同步.
同步代码块包括两个部分: 一个作为锁的对象引用, 一个作为由这个锁保护的代码块.
以关键字 synchronized
类修饰的方法就是一种横跨这个方法体的同步代码块, 其中该同步代码块的锁就是方法调用所在的对象. 静态的 synchronized
方法以 Class 对象作为锁.
synchronized(lock) {
// 方法或修改由锁保护的共享状态
}
每个 Java 对象都可以用做一个实现同步的锁, 这些锁被称为内置锁 或 监视器锁. 线程在进入同步代码块之前会自动获取锁, 并且在退出同步代码块时自动释放锁, 而无论是通过正常的控制路径退出, 还是通过从代码块中抛出异常退出. 获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法.
Java 的内置锁相当于一种 互斥锁, 最多只能有一个线程持有这个锁.
执行结果:
begin
线程 a 运行
end
begin
线程 b 运行
end
那么为什么加入 synchronized
关键字后就会同步呢? 这是因为关键字 synchronized
会取得一把对象锁, 而不是把一段代码或方法当做锁; 哪个线程先执行带 synchronized
关键字的方法, 哪个线程就持有该方法所属的对象的锁 Look, 那么其他线程只能呈等待状态.
这里有个前提是多个线程访问同一个对象, 下面演示的是多个线程访问不同的对象.
public class PrintObject {
synchronized public void printString(){
System.out.println("begin");
if(Thread.currentThread().getName().equals("a")){
PrintStream printStream = System.out;
printStream.println("线程 a 运行");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
}
}
if(Thread.currentThread().getName().equals("b")){
System.out.println("线程 b 运行");
}
System.out.println("end");
}
}
public static void main(String[] a) {
PrintObject pb = new PrintObject();
PrintObject pb1 = new PrintObject();
Thread thread1 = new Thread(pb::printString);
thread1.setName("a");
thread1.start();
Thread thread2 = new Thread(pb1::printString);
thread2.setName("b");
thread2.start();
}
执行结果
begin
线程 a 运行
begin
线程 b 运行
end
让 a 线程睡眠 100000 毫秒, 可以看到 a 线程并没有执行完, b 线程就运行了. 这也能够证明 synchronized
关键字取得是对象锁.
另外还需要注意一点, 我们使用两个线程执行同一对象的不同同步方法时, 如果线程 a 在睡眠, 那么线程 b 也会一直等待, 线程 a 执行完毕后再去执行.
注: 同步方法一定是线程安全的.
synchronized 锁重入
如果一个获取锁的线程调用其它的synchronized修饰的方法, 会发生什么?
在一个线程使用synchronized方法时调用该对象另一个synchronized方法, 即一个线程得到一个对象锁后再次请求该对象锁, 是永远可以拿到锁的.
在Java内部, 同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行, 同一个线程对同一个对象锁是可重入的, 同一个线程可以获取同一把锁多次, 也就是可以多次重入. 原因是Java中线程获得对象锁的操作是以线程为单位的, 而不是以调用为单位的.
这种情况也可以发生在继承中, 也就是说子类的同步方法调用父类的同步方式时, 时可以锁重入的.
但是, 如果子类重写了父类的方法, 并没有使用 synchronized 关键字, 则同步就失效了. 因为子类重写父类的方法, 当我们调用方法执行代码时, 执行的是子类的方法, 所以变成了异步执行.
synchronized 同步代码块
public class PrintObject {
public synchronized void printString(){
try {
System.out.println(Thread.currentThread().getName() + " 执行");
System.out.println(Thread.currentThread().getName() + " 插入数据到数据库");
// 让线程休眠, 模拟出网络延时
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 共享数据减1");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 插入数据到数据库");
Thread.sleep(5000);
if (Thread.currentThread().getName().equals("b")) {
SimpleDateFormat df = new SimpleDateFormat("mm:ss");
System.out.println(df.format(new Date()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
PrintObject pb = new PrintObject();
Thread thread1 = new Thread(pb::printString);
thread1.setName("a");
Thread thread2 = new Thread(pb::printString);
thread2.setName("b");
SimpleDateFormat df = new SimpleDateFormat("mm:ss");
System.out.println(df.format(new Date()));
thread1.start();
thread2.start();
执行结果
47:34
a 执行
a 插入数据到数据库
a 共享数据减1
a 插入数据到数据库
b 执行
b 插入数据到数据库
b 共享数据减1
b 插入数据到数据库
48:04
我们上面这段程序两个线程全部执行完所用的时间为 30 秒, 这里可以看出同步方法存在一个很大的弊端.
就是说我们的某个线程开始执行方法时, 无论我们操作的是不是共享数据, 别的线程都会等待此线程释放锁. 然后继续执行.
可是我们在插入数据到数据库的时候, 并不是在操作共享数据, 那么我们有没有什么办法, 只同步操作共享数据的那部分代码呢?
我们就可以使用 synchronized 同步代码块, 将程序修改成下面样子.
public class PrintObject {
public void printString(){
try {
System.out.println(Thread.currentThread().getName() + " 执行");
System.out.println(Thread.currentThread().getName() + " 插入数据到数据库");
// 让线程休眠, 模拟出网络延时
Thread.sleep(5000);
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " 共享数据减1");
Thread.sleep(5000);
}
System.out.println(Thread.currentThread().getName() + " 插入数据到数据库");
Thread.sleep(5000);
if (Thread.currentThread().getName().equals("b")) {
SimpleDateFormat df = new SimpleDateFormat("mm:ss");
System.out.println(df.format(new Date()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果
54:12
b 执行
a 执行
b 插入数据到数据库
a 插入数据到数据库
a 共享数据减1
a 插入数据到数据库
b 共享数据减1
b 插入数据到数据库
54:32
减少了10秒的执行时间, 提高了执行效率.
同步方法和同步代码块的锁都是同一把锁. 同步方法获取的是该方法的对象锁, 而同步代码块获取中的参数是 this, 表示当前对象. 所以获取的是同一把锁.
静态同步 synchronized 方法与 synchronized(class) 代码块
synchronized 关键字可以应用在 static 静态方法上, 表示当前的 *.java 文件对应的 Class 类进行持锁.
虽然运行结果与 synchronized 关键字加到非 static 静态方法上的结果类似, 但是是对 Class 类进行加锁, 而 Class 锁可以对类的所有对象起作用.
synchronized (DemoApplication.class) {
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。