Java SE基础(二十)多线程1

概述

之前写的代码无论是顺序结构,亦或选择结构、循环结构,都是同一时间内只能执行一个操作,并不能实现同一时间同时做多件事。日常生活中,我们并不希望下载工具(如:迅雷、网盘)下载资源并堵塞IO造成同一时间无法使用计算机,我们更希望可以同时听歌,下载,跑渲染。如何实现这个功能?这就需要多进程或多线程。

并发与并行

并发:同一时间段(比如10s内),多(>=2)个事件同时发生。

并行:同一时刻(ms级以内的时间片),多(>=2)个事件同时发生。

宏观上,很多程序看起来是同时运行的(并发),甚至给用户的感觉是在同时执行(理论上的并行),但微观上程序只在每个时间片上运行,即微观上程序只是在时间片上分时交替运行,∵CPU的频率很高,切换速度很快,造成了这种假象。

在多核心计算机上,多个核心事实上是在同时运行多个进程(例如老款i5,4核
线程,就是同时运行4个进程),属于并行。采用超线程技术的CPU,如老款i7为4核8线程,实际上是把空闲时间片抽取出来加以利用多出4个虚拟线程,看起来像是8进程并行。

单核心计算机一定不是并行的,只能是多个任务并发运行,且由于线程调度需要时间,单核心计算机使用并发运行反倒会降低效率(多核心CPU是核心越多,可同时执行的进程越多,可以提高效率)。但是并发运行较单线程,可以“同时”执行多个任务,时间片的利用率提高了,线程调度造成的效率损失也是值得的。

线程与进程

进程:在内存中运行的程序,每个进程都有独立的内存空间,一个应用程序有>=1个进程(用以实现多个功能)。系统运行程序就是进程创建、运行、消亡的过程。Java程序中,进程的堆内存与栈内存都是独立的。

线程:一个进程有>=1个线程,Java程序中,同一个进程中的线程有各自的栈内存,同时会共享堆内存,故线程消耗的资源比进程少很多。

JVM运行时,至少有主线程【main()方法的线程】与GC线程【垃圾回收线程】。

JVM的多线程调度机制是抢占式调度,没有分时调度,故执行结果有随机性。

使用

线程创建及启动

Java使用java.lang.Thread类代表线程,所有线程对象都是Thread类或子类的实例。线程的作用就是按照面向过程的方式按照代码顺序(线程执行体)执行任务。

①先继承Thread类,并重写run()方法(方法体中重写需要执行的代码段),获得线程类的子类(自定义线程类)。

public class PrimeThread extends Thread {//子类
    @Override
    public void run(){//方法体
        for (int i = 0; i < 20; i++) {
            System.out.println(getName()+":"+i);
        }
    }
}

主方法中实例化对象,使用start()方法即可启动。

public class Run {
    public static void main(String[] args) {
    PrimeThread pt1 = new PrimeThread();//实例对象
    PrimeThread pt2 = new PrimeThread();
    pt1.start();
    pt2.start();
    }
}

执行后:

Thread-1:0
Thread-0:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-1:5
Thread-1:6
Thread-1:7
Thread-1:8
Thread-1:9
Thread-1:10
Thread-1:11
Thread-1:12
Thread-1:13
Thread-1:14
Thread-1:15
Thread-1:16
Thread-1:17
Thread-1:18
Thread-1:19
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
Thread-0:10
Thread-0:11
Thread-0:12
Thread-0:13
Thread-0:14
Thread-0:15
Thread-0:16
Thread-0:17
Thread-0:18
Thread-0:19

Process finished with exit code 0

可以看出多线程由于抢占式调度,CPU执行时有随机性,出现了乱序。

②先实现接口,并重写run()方法,获得线程任务类。

public class PrimeRunnable implements Runnable {
    @Override
    public void run(){//方法体
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread()+":"+i);
        }
    }
}

主方法中实例化对象,可以直接使用run()方法运行,也可以将线程任务类的实例对象作为参数传入Thread类的构造方法并执行start()方法运行:

public class Run {
    public static void main(String[] args) {
        PrimeRunnable pr1 = new PrimeRunnable();
        PrimeRunnable pr2 = new PrimeRunnable();

        pr1.run();

        new Thread((pr1)).start();
        new Thread((pr2)).start();

    }
}

运行后:

Thread[main,5,main]:0
Thread[main,5,main]:1
Thread[main,5,main]:2
Thread[main,5,main]:3
Thread[main,5,main]:4
Thread[main,5,main]:5
Thread[main,5,main]:6
Thread[main,5,main]:7
Thread[main,5,main]:8
Thread[main,5,main]:9
Thread[Thread-0,5,main]:0
Thread[Thread-0,5,main]:1
Thread[Thread-0,5,main]:2
Thread[Thread-0,5,main]:3
Thread[Thread-0,5,main]:4
Thread[Thread-1,5,main]:0
Thread[Thread-1,5,main]:1
Thread[Thread-1,5,main]:2
Thread[Thread-0,5,main]:5
Thread[Thread-1,5,main]:3
Thread[Thread-0,5,main]:6
Thread[Thread-0,5,main]:7
Thread[Thread-0,5,main]:8
Thread[Thread-0,5,main]:9
Thread[Thread-1,5,main]:4
Thread[Thread-1,5,main]:5
Thread[Thread-1,5,main]:6
Thread[Thread-1,5,main]:7
Thread[Thread-1,5,main]:8
Thread[Thread-1,5,main]:9

Process finished with exit code 0

线程任务类可以作为线程类构造方法参数的原因

ctrl+鼠标左键进入start方法:

public synchronized void start() {
        /**
         * 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 */
            }
        }
    }

发现start方法通过某些条件的判断,尝试执行start0()方法,按照惯例,Java种带0的大概率是本地方法。再次定位,发现:

private native void start0();

果然,native修饰,调用了本地方法。显然是因为Java没有指针,不能像C、C++那样直接操作硬件寄存器,运用JNI(java native interface )技术,调用了其它语言的代码(包括但不限于C、汇编),与其它语言进行了交互,以便执行CPU抢占这种设计硬件操作的功能。。。

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    /**
     * This method is called by the system to give a Thread
     * a chance to clean up before it actually exits.
     */

发现了这么一段有趣的代码。。。

显然,start0()方法被执行后完成抢占CPU的操作,之后执行run()方法,且run()方法被重写,如果传入的对象非空即执行实参(对象)重写的run()方法。

    /* What will be run. */
    private Runnable target;

target是这么个玩意儿。。。再次进入Runnable查看:

package java.lang;

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * <p>
 * In addition, <code>Runnable</code> provides the means for a class to be
 * active while not subclassing <code>Thread</code>. A class that implements
 * <code>Runnable</code> can run without subclassing <code>Thread</code>
 * by instantiating a <code>Thread</code> instance and passing itself in
 * as the target.  In most cases, the <code>Runnable</code> interface should
 * be used if you are only planning to override the <code>run()</code>
 * method and no other <code>Thread</code> methods.
 * This is important because classes should not be subclassed
 * unless the programmer intends on modifying or enhancing the fundamental
 * behavior of the class.
 *
 * @author  Arthur van Hoff
 * @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

原来是个接口,里边还有个抽象方法run()。。。

再进Thread看看:

public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

    /**
     * Allocates a new {@code Thread} object so that it has {@code target}
     * as its run object, has the specified {@code name} as its name,
     * and belongs to the thread group referred to by {@code group}.
     *
     * <p>If there is a security manager, its
     * {@link SecurityManager#checkAccess(ThreadGroup) checkAccess}
     * method is invoked with the ThreadGroup as its argument.
     *
     * <p>In addition, its {@code checkPermission} method is invoked with
     * the {@code RuntimePermission("enableContextClassLoaderOverride")}
     * permission when invoked directly or indirectly by the constructor
     * of a subclass which overrides the {@code getContextClassLoader}
     * or {@code setContextClassLoader} methods.
     *
     * <p>The priority of the newly created thread is set equal to the
     * priority of the thread creating it, that is, the currently running
     * thread. The method {@linkplain #setPriority setPriority} may be
     * used to change the priority to a new value.
     *
     * <p>The newly created thread is initially marked as being a daemon
     * thread if and only if the thread creating it is currently marked
     * as a daemon thread. The method {@linkplain #setDaemon setDaemon}
     * may be used to change whether or not a thread is a daemon.
     *
     * @param  group
     *         the thread group. If {@code null} and there is a security
     *         manager, the group is determined by {@linkplain
     *         SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}.
     *         If there is not a security manager or {@code
     *         SecurityManager.getThreadGroup()} returns {@code null}, the group
     *         is set to the current thread's thread group.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this thread's run method is invoked.
     *
     * @param  name
     *         the name of the new thread
     *
     * @throws  SecurityException
     *          if the current thread cannot create a thread in the specified
     *          thread group or cannot override the context class loader methods.
     */

原来Thread类的构造方法是传入了实现接口的实例对象。

public class Thread {
            private Runnable target = new PrimeRunnable();

            public Thread(Runnable target) {
                this.target = target;
            }

            //start方法执行后执行start0方法,由JVM调用run方法
            public void run() {
                if (target != null) {
                    target.run();
                }
            }
        }

简单讲述,应该是这样。多态,编译看左边,执行看右边,执行的是PrimeRunable的Run方法(实现类的实例对象的方法)。如果没有重写run方法,就会执行jvm自己的run方法。。。而继承了Thread类的自定义线程类的实例对象,调用的就是自己已经重写的run()方法。

new Thread(pr).start();

pr.run();

这就解释了为什么可以将自定义线程的实例对象作为参数传入Thread类的构造方法,并且使用start()方法后和直接调用自己的run()方法效果一致。。。

匿名方式创建

正常的实现接口,并执行线程任务类的实例对象是这样3步走:

Runnable run1 = new Runnable(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        };
        Thread thread1 = new Thread(run1,"刘备Ⅰ");
        thread1.start();

使用Lambda表达式简化后为:

Runnable run2 = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+i);
            }
        };

        Thread thread2 = new Thread(run1,"刘备Ⅱ");
        thread2.start();

对应地,匿名表达后简化为:

new Thread((new Runnable(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        }),"关羽Ⅰ").start();

使用Lambda表达式简化后为:

new Thread((() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+i);
            }
        }),"关羽Ⅱ").start();

可以看出,和C#的委托还是很像的。

匿名内部类的实现:

new Thread("张飞"){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(getName()+":"+i);
                }
            }
        }.start();

这2种方式都可以有效地减少大量代码段,极大地提示开发效率。

都扔主方法里执行后:

张飞:0
张飞:1
张飞:2
张飞:3
张飞:4
关羽Ⅰ0
关羽Ⅰ1
关羽Ⅰ2
关羽Ⅰ3
张飞:5
张飞:6
张飞:7
张飞:8
张飞:9
关羽Ⅰ4
关羽Ⅰ5
关羽Ⅰ6
关羽Ⅰ7
关羽Ⅰ8
关羽Ⅰ9
关羽Ⅱ0
关羽Ⅱ1
关羽Ⅱ2
关羽Ⅱ3
关羽Ⅱ4
关羽Ⅱ5
关羽Ⅱ6
关羽Ⅱ7
关羽Ⅱ8
关羽Ⅱ9
刘备Ⅰ0
刘备Ⅰ1
刘备Ⅰ2
刘备Ⅰ3
刘备Ⅰ4
刘备Ⅰ5
刘备Ⅰ6
刘备Ⅰ7
刘备Ⅰ8
刘备Ⅰ9
刘备Ⅱ0
刘备Ⅱ1
刘备Ⅱ2
刘备Ⅱ3
刘备Ⅱ4
刘备Ⅱ5
刘备Ⅱ6
刘备Ⅱ7
刘备Ⅱ8
刘备Ⅱ9

Process finished with exit code 0

放后边的线程由于创建对象需要时间,本例重复次数少,看起来大体上是从上到下执行的。

Thread和Runnable的区别

类是一种关系设计,对象实例化时只是通过设计关系进行,由于类会被加载到方法区,对象会被加载到堆内存,实现Runnable接口就要比继承Thread类具有很多优势:

适合多个相同的程序代码的线程去共享同一个资源。
可以避免java中的单继承的局限性。
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
线程池只能放入(任务类)实现Runable或Callable类线程,不能直接放入继承Thread的类。

如果一个类继承Thread,则不适合资源共享(使用static修饰也可以实现共享资源)。但是如果实现了Runable接口的话,则很容易的实现资源共享。

多线程原理

先定义个自定义线程类:

public class MyThread extends Thread {
    public MyThread(String name) {//子类构造方法
        super(name);//传递给父类方法,父类getName时会获取到该name
    }

    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println(getName()+i);
        }

    }
}

主方法中执行:

public class Run {
    public static void main(String[] args) {
        System.out.println("Mian线程开始");

        new MyThread("关羽").start();//开启新线程

        for (int i = 0; i < 20; i++) {
            System.out.println("张飞"+i);
        }

    }
}

执行后:

Mian线程开始
Mian线程开始
张飞0
张飞1
张飞2
张飞3
张飞4
张飞5
关羽0
关羽1
张飞6
张飞7
张飞8
张飞9
关羽2
关羽3
关羽4
张飞10
张飞11
张飞12
张飞13
张飞14
张飞15
张飞16
张飞17
关羽5
张飞18
关羽6
关羽7
张飞19
关羽8
关羽9
关羽10
关羽11
关羽12
关羽13
关羽14
关羽15
关羽16
关羽17
关羽18
关羽19

Process finished with exit code 0

显然是乱序的,且每次执行后结果都不相同。以此为例讲述多线程的原理。

程序运行时,先加载字节码文件,在方法区创建main()方法,之后System.out.println("Mian线程开始");压栈并输出。接着new MyThread("关羽")被压栈,在堆内存中开辟空间并产生该匿名对象的数据,并单独开辟一个自定义线程的栈空间。调用start()方法后,自定义线程的方法体被压栈,同时主方法栈空间中的栈顶方法被弹栈,2个栈内存都进行了压栈及CPU资源抢占,在多核心计算机上开始并行(当然也可能抢占到同一个核心,在微观上变成分时交替串行)。先执行完成的线程会先弹栈,栈内的方法都执行完成后,线程在栈内存就会被释放,所有线程都执行完成后,进程也就结束了。

先看个售票的案例:

public class TicketRunnable implements Runnable {
    private int tickets = 100;//100张票

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//此处只能捕获异常,不能抛异常
                    e.printStackTrace();
                }//人为降低频率,频率越低出错率越高,频繁出现重复票


                System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets+"张票");
                tickets--;
            }
            else break;
        }
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        TicketRunnable tr = new TicketRunnable();

        new Thread(tr,"窗口1").start();
        new Thread(tr,"窗口2").start();
        new Thread(tr,"窗口3").start();

        //3个线程使用的任务类是同一个,执行的run方法也是同一个对象中的。。。操作的票数也是同一个。

        //Thread.sleep(5000);//使线程睡眠5s,人为降低线程的切换频率
    }  
}

某次运行后:

/*
窗口1正在卖第100张票
窗口1正在卖第99张票 //多次卖第99张票
窗口3正在卖第99张票 //多次卖第99张票
窗口3正在卖第97张票
窗口3正在卖第96张票
窗口3正在卖第95张票
窗口3正在卖第94张票
窗口3正在卖第93张票
窗口2正在卖第99张票 //多次卖第99张票
窗口3正在卖第92张票
窗口1正在卖第98张票
窗口3正在卖第90张票
窗口2正在卖第91张票
窗口3正在卖第88张票
窗口3正在卖第86张票
窗口3正在卖第85张票
窗口3正在卖第84张票
窗口3正在卖第83张票
窗口1正在卖第89张票
窗口3正在卖第82张票
窗口2正在卖第87张票
窗口3正在卖第80张票
窗口1正在卖第81张票
窗口1正在卖第77张票
窗口1正在卖第76张票
窗口1正在卖第75张票
窗口1正在卖第74张票
窗口1正在卖第73张票
窗口3正在卖第78张票
窗口3正在卖第71张票
窗口2正在卖第79张票
窗口3正在卖第70张票
窗口1正在卖第72张票
窗口1正在卖第67张票
窗口1正在卖第66张票
窗口1正在卖第65张票
窗口1正在卖第64张票
窗口1正在卖第63张票
窗口3正在卖第68张票
窗口2正在卖第69张票
窗口3正在卖第61张票
窗口3正在卖第59张票
窗口3正在卖第58张票
窗口3正在卖第57张票
窗口3正在卖第56张票
窗口3正在卖第55张票
窗口3正在卖第54张票
窗口3正在卖第53张票
窗口3正在卖第52张票
窗口3正在卖第51张票
窗口3正在卖第50张票
窗口3正在卖第49张票
窗口3正在卖第48张票
窗口3正在卖第47张票
窗口3正在卖第46张票
窗口3正在卖第45张票
窗口3正在卖第44张票
窗口3正在卖第43张票
窗口3正在卖第42张票
窗口3正在卖第41张票
窗口3正在卖第40张票
窗口3正在卖第39张票
窗口3正在卖第38张票
窗口3正在卖第37张票
窗口3正在卖第36张票
窗口3正在卖第35张票
窗口3正在卖第34张票
窗口3正在卖第33张票
窗口3正在卖第32张票
窗口3正在卖第31张票
窗口3正在卖第30张票
窗口3正在卖第29张票
窗口3正在卖第28张票
窗口3正在卖第27张票
窗口3正在卖第26张票
窗口3正在卖第25张票
窗口3正在卖第24张票
窗口3正在卖第23张票
窗口3正在卖第22张票
窗口3正在卖第21张票
窗口3正在卖第20张票
窗口3正在卖第19张票
窗口3正在卖第18张票
窗口1正在卖第62张票
窗口3正在卖第17张票
窗口3正在卖第15张票
窗口2正在卖第60张票
窗口1正在卖第16张票
窗口2正在卖第14张票
窗口1正在卖第13张票
窗口2正在卖第12张票
窗口2正在卖第10张票
窗口1正在卖第11张票
窗口2正在卖第8张票
窗口2正在卖第6张票
窗口2正在卖第5张票
窗口2正在卖第4张票
窗口2正在卖第3张票
窗口2正在卖第2张票
窗口2正在卖第1张票
窗口3正在卖第9张票
窗口1正在卖第7张票
 */

某次运行后:
在这里插入图片描述
除了重复票,居然还出现了负数票!!!

先来分析下产生重复票与负数票的原因。

字节码文件被加载到方法区后,TicketRunnable tr = new TicketRunnable();被压到主方法的栈内存,之后在堆内存开辟空间存储数据,并将声明的变量指向堆内存数据的地址。之后new Thread(tr,"窗口1").start(); new Thread(tr,"窗口2").start(); new Thread(tr,"窗口3").start();依次被【压栈、在堆内存中开辟空间并产生该匿名对象的数据,并单独开辟一个自定义线程的栈空间、调用start()方法后自定义线程的方法体被压栈(同时主方法栈空间中的栈顶方法被弹栈)】,3个副线程的栈内存都进行了压栈及CPU资源抢占,如果此时某个线程尚未来得及执行tickets--;其它进程已经开始执行System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets+"张票");就会出现重复票的情况。如果此时某线程执行System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets+"张票");前就已经有多个线程执行tickets--;就会出现漏票的情况;当tickets=1的时候,继续出现这种情况,就会出现负票的情况。

为了保证数据的安全性,就要加锁,防止线程抢占使得数据运算出错。

加锁

使用synchronized(唯一对象),就可以+锁。

private Object lock = new Object();
synchronized (lock){}

synchronized (this) this是唯一的,指向本对象自己,也可以用字符串等,只要是唯一的对象就可以。

上述案例在2处+锁都是可行的。

如果把锁+到if的方法体中,会直接运行,无视锁的影响,锁起不到作用。

public class TicketRunnable implements Runnable {
    private int tickets = 100;//100张票

    @Override
    public void run() {

            while (true) {
                synchronized (this){//锁+到此处就可以保证多个线程都能运行
                    if (tickets > 0) {//锁+到此处对解决线程安全性问题没什么卵用,依然存在负票的可能性
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {//此处只能捕获异常,不能抛异常
                            e.printStackTrace();
                        }//人为降低频率,频率越低出错率越高,频繁出现重复票


                        System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                        tickets--;
                    } else break;
                }

            }

        //}
    }
}

普通的同步方法

抽取并修改为同步方法

public class TicketRunnable implements Runnable {
    private int tickets = 100;//100张票

    @Override
    public void run() {
        while (true) {
            SellTicket();
            if(tickets <=0){
                break;
            }

        }
    }

    private synchronized void SellTicket() {
        //this普通的同步方法,锁对象是this

        if (tickets > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {//此处只能捕获异常,不能抛异常
                e.printStackTrace();
            }//人为降低频率,频率越低出错率越高,频繁出现重复票


            System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets+"张票");
            tickets--;
        }
        else return;
    }
}

this普通的同步方法,锁对象是this。

也可以实现锁的功能。

静态的同步方法

普通的锁对象就是this
静态方法得到锁对象是 当前类
public class ThreadTest {
    public static void show1() {//+锁synchronized
        synchronized (ThreadTest.class){//不能使用this,会报错
            System.out.print("a");
            System.out.print("b");
            System.out.print("c");
            System.out.print("d");
            System.out.print("e");
            //System.out.println("f");
            System.out.println("**************");
        }

    }

    public static synchronized void show2() {//+锁synchronized
        System.out.print("A");
        System.out.print("B");
        System.out.print("C");
        System.out.print("D");
        System.out.print("E");
        //System.out.println("F");
        System.out.println("--------------");
    }
}

在这里插入图片描述
使用this会报错,静态同步方法,锁对象是当前的这个类的字节码对象,由于类只会在方法区中被加载一次,一定是唯一的。

死锁

public class ThreadTest {
    private Object lockA = new Object();
    private Object lockB = new Object();

    public void showA(){
        synchronized (lockA){
            System.out.println("线程持有A等待B...");
            synchronized (lockB){
                System.out.println("线程持有A也持有B...");
            }
        }
    }

    public void showB(){
        synchronized (lockB){
            System.out.println("线程持有B等待A...");
            synchronized (lockA){
                System.out.println("线程持有B也持有A...");
            }
        }
    }
}
public class Run {
    public static void main(String[] args) {
        ThreadTest tt = new ThreadTest();

        new Thread(){
            @Override
            public void run(){
                tt.showA();
            }
        }.start();

        new Thread(){
            @Override
            public void run(){
                tt.showB();
            }
        }.start();
    }
}

运行后:

线程持有B等待A...
线程持有A等待B...

直接死锁。。。

线程持有A等待B...
线程持有A也持有B...
线程持有B等待A...
线程持有B也持有A...

Process finished with exit code 0

死锁有时候也会正常运行。。。

这种同步代码块嵌套的行为,可能由于CPU被抢占资源后,多个线程随机执行,多个线程同时抢占多把锁,造成相关线程全部阻塞无法运行。这种情况必须避免。

没想到多线程的内容这么多。。。再写一篇。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值