资讯专栏INFORMATION COLUMN

Java 多线程中篇

snowell / 1184人阅读

摘要:异步线程运行线程运行我们创建两个线程来调用同一业务对象的相同功能时可以看到下面输出线程运行线程运行两个线程在一起执行方法并且交叉打印也就是说当我们启动一个线程执行某个方法的时候就是异步执行至于为啥要这样演示是因为下面的同步同步将方法上加入关

异步
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 关键字, 来使方法同步.

执行结果:

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) {
            
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73654.html

相关文章

  • 【page-monitor 前端自动化 中篇】 源码分析

    摘要:上篇中初探了的一些功能和在前端自动化测试方面的可行性,本篇主要分析下的实现方式和源码。文件分析完整文件目录运行生成目录分析出了及其组件代码,可用和值的分析的文件和下面的五个文件。相关文章前端自动化上篇初步调研前端自动化下篇实践应用 上篇中初探了page-monitor的一些功能和在前端自动化测试方面的可行性,本篇主要分析下page-monitor的实现方式和源码。 mode-modul...

    Object 评论0 收藏0
  • Android网络编程8之源码解析OkHttp中篇[复用连接池]

    摘要:构造函数默认空闲的最大连接数为个,的时间为秒通过构造函数可以看出默认的空闲的最大连接数为个,的时间为秒。实例化实例化是在实例化时进行的在的构造函数中调用了省略省略缓存操作提供对进行操作的方法分别为和几个操作。 1.引子 在了解OkHttp的复用连接池之前,我们首先要了解几个概念。 TCP三次握手 通常我们进行HTTP连接网络的时候我们会进行TCP的三次握手,然后传输数据,然后再释放连接...

    fasss 评论0 收藏0
  • Java 虚拟机总结给面试的你(下)

    摘要:本篇博客主要针对虚拟机的晚期编译优化,内存模型与线程,线程安全与锁优化进行总结,其余部分总结请点击虚拟总结上篇,虚拟机总结中篇。 本篇博客主要针对Java虚拟机的晚期编译优化,Java内存模型与线程,线程安全与锁优化进行总结,其余部分总结请点击Java虚拟总结上篇 ,Java虚拟机总结中篇。 一.晚期运行期优化 即时编译器JIT 即时编译器JIT的作用就是热点代码转换为平台相关的机器码...

    amc 评论0 收藏0
  • 前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【下篇】

    摘要:安装后已经完成了安装,并且等待其他的线程被关闭。激活后在这个状态会处理事件回调提供了更新缓存策略的机会。并可以处理功能性的事件请求后台同步推送。废弃状态这个状态表示一个的生命周期结束。 showImg(https://segmentfault.com/img/bVbwWJu?w=2056&h=1536); 不知不觉,已经来到了最后的下篇 其实我写的东西你如果认真去看,跟着去写,应该能有...

    fireflow 评论0 收藏0
  • 前端20个灵魂拷问 彻底搞明白你就是中级前端工程师 【中篇

    摘要:前端个灵魂拷问,彻底搞明白你就是中级前端工程师上篇感觉大家比较喜欢看这种类型的文章,以后会多一些。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。此规范其实是在推广过程中产生的。 showImg(https://segmentfault.com/img/bVbwAMU?w=700&h=394); 前端20个灵魂拷问,彻底搞明白你就是中级前端工程师...

    MartinDai 评论0 收藏0

发表评论

0条评论

snowell

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<