多线程编程之线程的同步机制(上): Synchronized同步方法

简介: 多线程中的同步,指的是如何开发出线程安全的程序或者应用,也就是得解决非线程安全所带来的一些相关问题-----脏读。

1 前言


多线程中的同步,指的是如何开发出线程安全的程序或者应用,也就是得解决非线程安全所带来的一些相关问题-----脏读


线程安全非线程安全是学习多线程编程以及日常开发时一定会遇到的问题。非线程安全其实当多个线程访问同一个对象中的成员变量时产生的,产生的后果就是脏读,就是取到的数据其实是被更改过的。而线程安全就是以获取的成员变量的值是经过同步处理的,不会出现脏读的现象。


说到线程同步就必须得说到java中的synchronized关键字。


Synchronized关键字是一种同步锁解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。其他线程 必须等待当前线程执行完该方法(代码块)后才能执行该方法 (代码块)。


所以 Synchronized关键字有两个作用:


1、Synchronized同步方法


2、Synchronized同步代码块


这块的内容比较长,所以这里分为两个部分来写。这篇文章就先写Synchronized同步方法


本章内容有:


1、局部变量是线程安全的


2、成员变量不是线程安全的


3、多个对象使用多个对象锁


4、synchronized方法锁的是整个对象


5、脏读


6、锁重入


7、锁的自动释放


8、同步不具有继承性


2 正文


1、局部变量是线程安全的


局部变量不存在非线程安全问题,永远是线程安全的,这是由于局部变量是私有的所造成的。


即线程a修改局部变量,不会影响线程b的访问。比如:


package com.jiangxia.chap2;
/**
 * 线程同步机制
 * author:jiangxia
 * date:2021-04-05
 */
public class Demo1 {
    public static void main(String[] args) {
        Servie1 servie1 = new Servie1();
        Thread t1 = new Thread1(servie1);
        t1.start();
        Thread t2 = new Thread2(servie1);
        t2.start();
    }
}
class Servie1{
    public void set(String string){
        int num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");
            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);
    }
}
class Thread1 extends  Thread{
    private Servie1 servie1;
    public Thread1(Servie1 servie1){
        this.servie1 = servie1;
    }
    @Override
    public void run() {
        servie1.set("a");
    }
}
class Thread2 extends  Thread{
    private Servie1 servie1;
    public Thread2(Servie1 servie1){
        this.servie1 = servie1;
    }
    @Override
    public void run() {
        servie1.set("b");
    }
}
复制代码
复制代码


bcf89730e6c74bc2b1a2422cdd9b74a7~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


2、成员变量不是线程安全的


在java中成员变量不是线程安全的。这里顺便说下什么是成员变量以及局部变量。


局部变量:在方法内定义的变量称为“局部变量”或“临时变量”,方法结束后局部变量占用的内存将被释放。


成员变量:在类体的变量部分中定义的变量。


局部变量和成员变量主要是他们作用域的区别


成员变量是类内部;


局部变量是定义在其方法体内部(或者方法体内部的某一程序块内)。


成员变量可以不显式初始化,它们可以由系统设定默认值;


局部变量没有默认值,所以必须设定初始赋值。


在内存中的位置也不一样。成员变量在所在类被实例化后,存在堆内存中;局部变量在所在方法调用时,存在栈内存空间中。

代码:


package com.jiangxia.chap2;
/**
 * 线程的同步机制 成员变量不是线程安全的
 * author:jiangxia
 */
public class Demo2 {
    public static void main(String[] args) {
        Servie2 s = new Servie2();
        Thread t1 = new ThreadA(s);
        t1.start();
        Thread t2 = new ThreadB(s);
        t2.start();
    }
}
class Servie2{
    private int num = 0;
    public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");
            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);
    }
}
class ThreadA extends Thread{
    private Servie2 servie2;
    public ThreadA(Servie2 servie2){
        this.servie2 = servie2;
    }
    @Override
    public void run() {
        servie2.set("a");
    }
}
class ThreadB extends  Thread{
    private Servie2 servie2;
    public ThreadB(Servie2 servie2){
        this.servie2 = servie2;
    }
    @Override
    public void run() {
        servie2.set("b");
    }
}
复制代码
复制代码


结果:


55862ad6d2334bcf9c62ade8b71e1b2e~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


如果对set方法设置synchronized关键字进行修饰,比如:


package com.jiangxia.chap2;
/**
 * 线程的同步机制 成员变量不是线程安全的
 * author:jiangxia
 */
public class Demo2 {
    public static void main(String[] args) {
        Servie2 s = new Servie2();
        Thread t1 = new ThreadA(s);
        t1.start();
        Thread t2 = new ThreadB(s);
        t2.start();
    }
}
class Servie2{
    private int num = 0;
    //set 方法设置synchronized关键字
    synchronized public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");
            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);
    }
}
class ThreadA extends Thread{
    private Servie2 servie2;
    public ThreadA(Servie2 servie2){
        this.servie2 = servie2;
    }
    @Override
    public void run() {
        servie2.set("a");
    }
}
class ThreadB extends  Thread{
    private Servie2 servie2;
    public ThreadB(Servie2 servie2){
        this.servie2 = servie2;
    }
    @Override
    public void run() {
        servie2.set("b");
    }
}
复制代码
复制代码


结果:


0e3df2e21fa645bb81b0eee8b3803e1b~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


如果有两个线程同时操作业务对象中的成员变量,可能会产生非线程安全的问题,所以需要在方法前使用synchronized关键字进行修饰。


3、多个对象使用多个对象锁


Synchronized取到的锁都是对象锁,而不是把一段代码或者方法作为锁,所以哪个线程先执行Synchronized关键字修饰的方法,那么哪个线程就持有该方法所属对象的锁,其他的线程只能等待,前提是多个线程访问的是同一个对象。如果多个线程访问的是多个对象,那么jvm就会创建出多个对象锁。比如:


package com.jiangxia.chap2;
public class Demo3 {
    public static void main(String[] args) {
        Servie3 s1 = new Servie3();
        Servie3 s2 = new Servie3();
        Thread t1 = new Demo3ThreadA(s1);
        Thread t2 = new Demo3ThreadB(s2);
        t1.start();
        t2.start();
    }
}
class Servie3{
    private int num = 0;
    //set 方法设置synchronized关键字
    synchronized public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");
            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);
    }
}
class Demo3ThreadA extends Thread{
    private Servie3 servie3;
    public Demo3ThreadA(Servie3 servie3){
        this.servie3 = servie3;
    }
    @Override
    public void run() {
        servie3.set("a");
    }
}
class Demo3ThreadB extends  Thread{
    private Servie3 servie3;
    public Demo3ThreadB(Servie3 servie3){
        this.servie3 = servie3;
    }
    @Override
    public void run() {
        servie3.set("b");
    }
}
复制代码
复制代码


结果:


ce6aab7ca8a9447084c08cda49c6bccb~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


4、Synchronized方法锁的是整个对象


比如:


package com.jiangxia.chap2;
public class Demo4 {
    public static void main(String[] args) {
        Demo4Service service = new Demo4Service();
        Thread t1 = new Demo4ThreadA(service);
        Thread t2 = new Demo4ThreadB(service);
        t1.start();
        t2.start();
    }
}
class Demo4Service{
    synchronized public void foo1(){
        System.out.println("开始运行foo1方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo1方法运行结束");
    }
    synchronized public void foo2(){
        System.out.println("开始运行foo2方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo2方法运行结束");
    }
}
class Demo4ThreadA extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadA(Demo4Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.foo1();
    }
}
class Demo4ThreadB extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadB(Demo4Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.foo2();
    }
}
复制代码
复制代码


结果:


8900cb8bc779410ab652b9167be77617~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


A线程先持有object对象的对象锁,B线程就不可以以异步的方式调用object对象使用synchronized关键字修饰的方法,线程B只能等待线程A的方法执行完成释放对象锁才能够执行,也就是同步执行。


B线程可以以异步的方式调用object对象没有使用synchronized修饰的方法。比如:


package com.jiangxia.chap2;
public class Demo4 {
    public static void main(String[] args) {
        Demo4Service service = new Demo4Service();
        Thread t1 = new Demo4ThreadA(service);
        Thread t2 = new Demo4ThreadB(service);
        t1.start();
        t2.start();
    }
}
class Demo4Service{
    synchronized public void foo1(){
        System.out.println("开始运行foo1方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo1方法运行结束");
    }
     public void foo2(){
        System.out.println("开始运行foo2方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo2方法运行结束");
    }
}
class Demo4ThreadA extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadA(Demo4Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.foo1();
    }
}
class Demo4ThreadB extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadB(Demo4Service service){
        this.service = service;
    }
    @Override
    public void run() {
        service.foo2();
    }
}
复制代码
复制代码


结果:


19535291081442c0bca123dcdff7cd1a~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


5、脏读


脏读表示在多个线程共享同一个对象,进行了修改对象中属性,当读取对象属性的时候,发现属性被别的线程修改了.


假设一个很简单的例子,当一个被共享的对象有A,B,同时有setValue和geValue方法分别去修改和读取这两个值,当setValue方法加上同步锁synchronized,也就是说在修改的过程中是同步的,但是getValue方法不同步,也就是说当第一个进去的线程执行完了 再去执行getValue方法的时候,这个时候对象的两个属性值A,B可能被第二个进去的线程修改了,拿到的数据并不是想要的,这就是脏读。


比如:


package com.jiangxia.chap2;
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Demo05User user = new Demo05User();
        Thread t = new Demo05Thread(user);
        t.start();
        Thread.sleep(200);
        user.getValue();
    }
}
class Demo05User{
    private String username = "a";
    private String password = "aa";
    synchronized public void setUsernameAndPassword(String username, String password){
        this.username = username;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
        System.out.println("setUsernameAndPassword方法,线程名称:" + Thread.currentThread().getName() +
                ",username=" +username + ", password=" + password);
    }
    synchronized public void getValue(){
        System.out.println("getValue方法,线程名称" + Thread.currentThread().getName() +
                ", username=" + username + ", password=" + password );
    }
}
class Demo05Thread extends Thread{
    private Demo05User user;
    public Demo05Thread(Demo05User user){
        this.user = user;
    }
    @Override
    public void run() {
        user.setUsernameAndPassword("B", "BB");
    }
}
复制代码
复制代码


结果:


fc4ecf03327041aca51232ea0609ee37~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


package com.jiangxia.chap2;
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Demo05User user = new Demo05User();
        Thread t = new Demo05Thread(user);
        t.start();
        Thread.sleep(200);
        user.getValue();
    }
}
class Demo05User{
    private String username = "a";
    private String password = "aa";
    synchronized public void setUsernameAndPassword(String username, String password){
        this.username = username;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
        System.out.println("setUsernameAndPassword方法,线程名称:" + Thread.currentThread().getName() +
                ",username=" +username + ", password=" + password);
    }
     public void getValue(){
        System.out.println("getValue方法,线程名称" + Thread.currentThread().getName() +
                ", username=" + username + ", password=" + password );
    }
}
class Demo05Thread extends Thread{
    private Demo05User user;
    public Demo05Thread(Demo05User user){
        this.user = user;
    }
    @Override
    public void run() {
        user.setUsernameAndPassword("B", "BB");
    }
}
复制代码
复制代码


结果:


a814f0e6946f41debee968a987c6bfe0~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


当A线程调用使用synchronize关键字修饰的方法时,A线程就获取到一个方法锁,准确来说是获取到对象锁,所以其它线程必须等A线程执行完毕后才可以调用其它使用synchronized关键字修饰的方法。这时A线程已经执行完一个完整任务,也就是说username和password两个成员变量已经被同时赋值,所以这种情况下是不存在脏读的。


6、锁重入


可重入锁:自己可以再次获取自己的内部的锁。比如有线程A获得了某对象的锁,此时这个时候锁还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。可重入锁也支持在父子类继承的环境中。

关键字Synchronized拥有锁重入的功能,就是说在使用synchronized时,当一个线程得到一个对象锁后,再次请求对象锁时是可以再次得到该对象锁。比如:

package com.jiangxia.chap2;
public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo06Thread();
        t.start();
    }
}
class Demo06Service{
    synchronized public void foo1(){
        System.out.println("foo1方法");
        foo2();
    }
    synchronized public void foo2(){
        System.out.println("foo2方法");
        foo3();
    }
    synchronized public void foo3(){
        System.out.println("foo3方法");
    }
}
class Demo06Thread extends Thread{
    @Override
    public void run() {
        Demo06Service service = new Demo06Service();
        service.foo1();
    }
}
复制代码
复制代码


结果:


d24725427ae54a28af9e9bc59f8036d9~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


可重入锁也支持在父子类继承的环境中。比如:


public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo06Thread();
        t.start();
    }
}
class Demo06Service{
    synchronized public void foo1(){
        System.out.println("foo1方法");
        foo2();
    }
    synchronized public void foo2(){
        System.out.println("foo2方法");
        foo3();
    }
    synchronized public void foo3(){
        System.out.println("foo3方法");
    }
}
class Demo06Thread extends Thread{
    @Override
    public void run() {
//        Demo06Service service = new Demo06Service();
//        service.foo1();
        //改用继承子类
        Demo06ServiceB serviceB = new Demo06ServiceB();
        serviceB.foo4();
    }
}
//继承
class Demo06ServiceB extends Demo06Service{
    synchronized public void foo4(){
        System.out.println("foo4方法");
        super.foo1();
    }
}
复制代码
复制代码


结果:


ba92b1ecb1e948a1be1d5ba13932c21a~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


7、锁的自动释放


当一个线程执行的代码出现了异常,其所持有的锁应该会自动释放,比如:


public class Demo7 {
    public static void main(String[] args) {
        DemoService7 service7 = new DemoService7();
        Thread t1 = new DemoThread7(service7);
        t1.setName("A");
        t1.start();
        Thread t2 = new DemoThread7(service7);
        t2.start();
    }
}
class DemoService7{
    synchronized public void foo(){
        if("A".equals(Thread.currentThread().getName())){
            System.out.println("线程A开始于"+System.currentTimeMillis());
            while(true){
                if((""+ Math.random()).substring(0,8).equals("0.123456")){
                    System.out.println("线程A结束于:"+System.currentTimeMillis());
                    Integer.parseInt("A");
                }
            }
        }else{
            System.out.println("线程B开始于"+System.currentTimeMillis());
        }
    }
}
class DemoThread7 extends Thread{
    private DemoService7 service7;
    public DemoThread7(DemoService7 service7){
        this.service7 = service7;
    }
    @Override
    public void run() {
        service7.foo();
    }
}
复制代码
复制代码


结果:


4b0ad267e806471ebdf6e9b77630026f~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


8、同步不具有继承性


同步synchronized不具有继承性,比如:


public class Demo8 {
    public static void main(String[] args) {
        DemoService8B serviceB = new DemoService8B();
        Thread t1 = new Demo8Thread(serviceB);
        t1.setName("A");
        t1.start();
        Thread t2 = new Demo8Thread(serviceB);
        t2.setName("B");
        t2.start();
    }
}
class DemoService8A{
    synchronized public void foo(){
        try{
            System.out.println("父类:" + Thread.currentThread().getName() + ",开始于" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("父类:" + Thread.currentThread().getName() + ",结束于" + System.currentTimeMillis());
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}
class DemoService8B extends  DemoService8A{
    public void foo(){
        try {
            System.out.println("子类:" + Thread.currentThread().getName() + ",开始于" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("子类:" + Thread.currentThread().getName() + ",结束于" + System.currentTimeMillis());
            super.foo();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
class Demo8Thread extends Thread{
    DemoService8B service;
    public Demo8Thread(DemoService8B service){
        this.service = service;
    }
    @Override
    public void run() {
        service.foo();
    }
}
复制代码
复制代码


结果:


cfa2bfc030f94acd8d62daf264e1ae0d~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


3 总结


以上就是Java多线程编程中synchronized关键字修饰方法的一些内容,synchronized关键字不仅可以修饰方法,还可以修饰代码块。后面一篇文章将会介绍synchronized关键字修饰方法的缺点以及它修饰代码块的相关内容。

目录
相关文章
|
14天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
15天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
41 3
|
15天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
42 1
|
2月前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
75 2
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
50 4
|
2月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
107 2
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
28 2
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
70 0
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
65 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
43 3