多线程与同步代码块详解

举报
阿Q说代码 发表于 2022/04/12 09:44:09 2022/04/12
【摘要】 多线程并发执行可以提高程序的效率,可以同时完成多项工作,多线程并发执行的实质就是CPU在做着高速的切换。

线程是程序执行的一条路径, 一个进程中可以包含多条线程。多线程并发执行可以提高程序的效率,可以同时完成多项工作,多线程并发执行的实质就是CPU在做着高速的切换。多线程的应用场景:红蜘蛛同时共享屏幕给多个电脑;迅雷开启多条线程一起下载;QQ同时和多个人一起视频;服务器同时处理多个客户端请求。

并行和并发的区别

  • 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)
  • 并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
  • 比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。

Java程序运行原理

Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

代码验证:

public class Demo1_Thread {	//此程序的运行结果是 "我是主线程的执行代码"和"垃圾被清扫了"交替执行
    /**
    证明jvm是多线程的
  	 */
    public static void main(String[] args) {
        for(int i = 0; i < 100000; i++) {
            new Demo();
        }

        for(int i = 0; i < 10000; i++) {
            System.out.println("我是主线程的执行代码");
        }
    }
}

class Demo {
    @Override
    public void finalize() {
        System.out.println("垃圾被清扫了");
    }

}

多线程的实现

方式一:定义类继承Thread,重写run方法,把新线程要做的事写在run方法中;创建线程对象,开启(调用start())新线程, 内部会自动执行run方法。

代码演示:

public class Demo2_Thread {
    public static void main(String[] args) {
        MyThread mt = new MyThread();		//4,创建Thread类的子类对象
        mt.start();				//5,开启线程
        MyThread mt1 = new MyThread();
        mt1.start();

        for(int i = 0; i < 1000; i++) {
            System.out.println("bb");
        }
    }

}
class MyThread extends Thread {		//1,继承Thread
    public void run() {				//2,重写run方法
        for(int i = 0; i < 1000; i++) {		//3,将要执行的代码写在run方法中
            System.out.println("aaaaaaaaaaaa");
        }
    }
}

方式二:定义类实现Runnable接口,实现run方法,把新线程要做的事写在run方法中,创建自定义的Runnable的子类对象,创建Thread对象,传入Runnable,调用start()开启新线程,内部会自动调用Runnable的run()方法。

代码演示:

public class Demo3_Thread {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();	//4,创建Runnable的子类对象
        Thread t = new Thread(mr);		//5,将其当作参数传递给Thread的构造函数
        t.start();				//6,开启线程
        for(int i = 0; i < 1000; i++) {
            System.out.println("bb");
        }
    }

}
class MyRunnable implements Runnable {			//1,定义一个类实现Runnable

    @Override
    public void run() {				//2,重写run方法
        for(int i = 0; i < 1000; i++) {		//3,将要执行的代码写在run方法中
            System.out.println("aaaaaaaaaaaa");
        }
    }

}

剖析实现Runnable接口的源码

源码解释:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法。

源码解析:

public class Thread implements Runnable {	//Thread类本身实现了Runnable接口
    /*省略代码*/

    private Runnable target;	//一个Runnable类型的成员变量

    public Thread(Runnable target) {//Thread的有参构造方法,传入一个Runnable接口的子类对象target
        //调用init方法(下面这个) 把target传入
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {	
        //init方法里面又调用另一个重载的init方法(下面这个)把target传入
        init(g, target, name, stackSize, null);		
    }
    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
        /*省略代码*/
        this.target = target;//在init方法里面把成员变量target 进行赋值
        /*省略代码*/
    }
    public synchronized void start() {	//当调用start方法的时候 内部调用的是start0()方法
        /*省略代码*/
        start0();
        /*省略代码*/
    }

    private native void start0();	//start0()是native修饰的方法 底层是其他语言 我们无法查看,但是我们知道底层会调用run方法

    @Override
    public void run() {		//start0()的底层是会调用run()方法
        if (target != null) {	//判断成员变量target是否为null
            target.run();	//如果target被赋值了,就要调用target的run方法
        }
    }
    /*省略代码*/
}

多线程两种实现方式的区别

继承Thread : 由于子类重写了Thread类的run(),当调用start()时, 直接找子类的run()方法;好处是:可以直接使用Thread类中的方法,代码简单;弊端是:如果已经有了父类,就不能用这种方法。

实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法。好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的;弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂。

匿名内部类实现线程的两种方式

public static void main(String[] args) {
    new Thread() {								//1,继承Thread类
        public void run() {						//2,重写run方法
            for(int i = 0; i < 1000; i++) {		//3,将要执行的代码写在run方法中
                System.out.println("aaaaaaaaaaaaaa");
            }
        }
    }.start();									//4,开启线程

    new Thread(new Runnable() {					//1,将Runnable的子类对象传递给Thread的构造方法
        public void run() {						//2,重写run方法
            for(int i = 0; i < 1000; i++) {		//3,将要执行的代码写在run方法中
                System.out.println("bb");
            }
        }
    }).start();									//4,开启线程
}

public final String getName():获取线程对象的名称。默认情况下,名字的组成 Thread-编号(编号从0开始)
public final void setName(String name):设置线程名称。

代码演示:

//通过构造方法给name赋值
public static void demo1() {
    new Thread("芙蓉姐姐") {		//通过构造方法给name赋值
        public void run() {
            System.out.println(this.getName() + "....aaaaaaaaa");
        }
    }.start();
}
//通过setName()来设置线程的名字
public static void main(String[] args) {

    Thread t = new Thread() {
        public void run() {
            //this.setName("张三");		//在这儿设置也行  在外面设置名称也行
            System.out.println(this.getName() + "....aaaaaaaaaaaaa");
        }
    };

    t.setName("张三");	//通过setName()来设置线程的名字
    t.start();
}

public static Thread currentThread():返回当前正在执行的线程对象引用

public static void sleep(long millis):让当前线程休眠millis毫秒

public final void setDaemon(boolean on):设置线程为守护线程,一旦前台(主线程)结束,守护线程就结束了

public final void join():当前线程暂停,等待指定的线程执行结束后,当前线程再继续

public final void join(long millis):当前线程暂停, 等待指定的线程执行millis毫秒结束后, 当前线程再继续

public static void yield():暂停当前正在执行的线程对象,并执行其他线程。

public final int getPriority():获取线程优先级

public final void setPriority(int newPriority):更改线程的优先级。线程默认优先级是5。范围是1-10。

注意:优先级可以在一定的程度上,让线程获较多的执行机会。

代码演示:

public static void main(String[] args) {
    final Thread t1 = new Thread() {
        public void run() {
            for(int i = 0; i < 10; i++) {
                System.out.println(getName() + "...aaaaaaaaaaaaa");
            }
        }
    };

    Thread t2 = new Thread() {
        public void run() {
            for(int i = 0; i < 10; i++) {
                if(i == 2) {
                    try {
                        t1.join();	//t1插队后,t1执行结束后 t2才能接着执行
                        //t1.join(1);	//插队指定的时间,过了指定时间后,两条线程交替执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(getName() + "...bb");
            }
        }
    };
    t1.start();
    t2.start();
}

同步代码块

当多线程并发, 有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作,这时就需要同步。如果两段代码是同步的,那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码。所谓同步代码块就是使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块,多个同步代码块如果使用相同的锁对象,那么他们就是同步的。

代码演示

public class Demo_Synchronized {
    public static void main(String[] args) {
        //匿名内部类使用局部变量,局部变量前面必须加final修饰,为了延长局部变量的声明周期
        final Printer p = new Printer();	

        new Thread() {
            public void run() {
                while(true) {
                    p.print1();	//调用print1()
                }
            }
        }.start();

        new Thread() {
            public void run() {
                while(true) {
                    p.print2();	//调用print2()
                }
            }
        }.start();
    }

}

class Printer {
    Demo d = new Demo();
    public void print1() {	
        synchronized(d) {	//同步代码块,锁机制,锁对象可以是任意的
            //当多线程并发, 有多段代码同时执行时
            //我们希望下面代码执行的过程中CPU不要切换到其他线程工作
            System.out.print("黑");
            System.out.print("马");
            System.out.print("程");
            System.out.print("序");
            System.out.print("员");
            System.out.print("\r\n");
        }
    }

    public void print2() {						
        synchronized(d) {//如果说两块代码想同步的话,那么这两个同步代码块的锁对象必须是同一个锁对象,所以说上面的锁对象是d,这一个锁对象也是d
            //我们希望下面代码执行的过程中CPU不要切换到其他线程工作
            System.out.print("传");
            System.out.print("智");
            System.out.print("播");
            System.out.print("客");
            System.out.print("\r\n");
        }
    }
}

class Demo{}

同步方法

使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的,非静态同步函数的锁是当前对象this,静态的同步函数的锁是当前类的字节码对象。

代码演示:

public class Demo_Synchronized {
    public static void main(String[] args) {
        //匿名内部类使用局部变量,局部变量前面必须加final修饰,为了延长局部变量的声明周期
        final Printer2 p = new Printer2();

        new Thread() {
            public void run() {
                while(true) {
                    p.print1();
                }
            }
        }.start();

        new Thread() {
            public void run() {
                while(true) {
                    p.print2();
                }
            }
        }.start();
    }
}

class Printer2 {
    Demo d = new Demo();
    //非静态的同步方法的锁对象是神马?
    //答:非静态的同步方法的锁对象是this
    //静态的同步方法的锁对象是什么?
    //是该类的字节码对象
    public static synchronized void print1() {//同步方法只需要在方法上加synchronized关键字即可
        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.print("\r\n");
    }

    public static void print2() {
        synchronized(Printer2.class) {		
            System.out.print("传");
            System.out.print("智");
            System.out.print("播");
            System.out.print("客");
            System.out.print("\r\n");
        }
    }
}

死锁
同步代码块的嵌套就容易出现死锁。所以开发中尽量避免同步代码块的嵌套。

代码演示:

public class Demo5_DeadLock {
    /**
    @param args
  	*/
    private static String s1 = "筷子左";
    private static String s2 = "筷子右";

    public static void main(String[] args) {
        new Thread() {
            public void run() {
                while(true) {
                    synchronized(s1) {
                        System.out.println(getName() + "...获取" + s1 + "等待" + s2);
                        synchronized(s2) {
                            System.out.println(getName() + "...拿到" + s2 + "开吃");
                        }
                    }
                }
            }
        }.start();


        new Thread() {
            public void run() {
                while(true) {
                    synchronized(s2) {
                        System.out.println(getName() + "...获取" + s2 + "等待" + s1);
                        synchronized(s1) {
                            System.out.println(getName() + "...拿到" + s1 + "开吃");
                        }
                    }
                }
            }
        }.start();
    }
}

Vector,StringBuffer,Hashtable这些类之所以说是同步的,是因为他们里面的方法上都加了synchronized

  • Collections.synchroinzedXxx(xxx):可以返回一个线程安全的集合
  • public static <T> Collection<T> synchronizedCollection(Collection<T> c)
  • public static <T> Set<T> synchronizedSet(Set<T> s)
  • public static <T> List<T> synchronizedList(List<T> list)
  • public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

注:Vector是线程安全的,ArrayList是线程不安全的

  • StringBuffer是线程安全的,StringBuilder是线程不安全的
  • Hashtable是线程安全的,HashMap是线程不安全的

阿Q将持续更新java实战方面的文章,感兴趣的可以关注下公众号:阿Q说代码,也可以添加阿Q:qingqing-4132 进一步交流!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。