黑马程序员——多线程

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

多线程

1. 多线程的概述

1线程概念

           线程:就是程序中单独顺序的流控制

           线程本身不能运行,它只能用于程序中

           说明:线程是程序内的顺序控制流,只能使用分配给程序的资源和环境。

(2)进程

           进程:执行中的程序。程序是静态的概念,进程是动态的概念。

           一个进程可以包含一个或多个线程。

           一个进程至少要包含一个线程。

(3)线程与进程的区别:

          多个进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响。线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换负担比进程切换的负担要小。多线程程序比多进程程序需要更少的管理费用。

        进程是重量级的任务,需要分配给它们独立的地址空间,进程间通信是昂贵和受限的,进程间的转换也是很需要花费的。另一方面,线程是轻量级的选手,它们共享相同的地址空间并且共同分享同一个进程,线程间的通信是便宜的,线程间的转换也是低成本的。

(4)单线程

          单个程序中只有一个线程就是单线程。  

          当程序启动运行时,就自动产生一个线程,主方法main就在这个主线程上运行。我们的程序都是由线程来执行的。

(5)多线程

          多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务。

          多线程编程的目的,就是“最大限度地利用CPU资源”,当某一线程的处理不需要占用CPU而只和IO等资源打交道时,让需要占用CPU的其他线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。

          一个程序实现多个代码同时交替运行就需要产生多个线程。

          CPU随机地抽出时间,让我们的程序一会做这件事情,一会做另外的事情。

          从宏观角度来看,多个线程在同时执行(宏观并行),但是微观上来看,处理器的个数决定了某一个时刻可以同时运行的最大线程数,如单核CPU某一时刻只能有一个线程在执行(微观串行),双核的CPU在某一个时刻,最多可以运行两个线程,可以做到微观并行。

(6) 计算机CPU的运行原理

        我们电脑上有很多的程序在同时进行,就好像cpu在同时处理这所以程序一样。但是,在一个时刻,单核的cpu只能运行一个程序。而我们看到的同时运行效果,只是cpu在多个进程间做着快速切换动作。

       而cpu执行哪个程序,是毫无规律性的。这也是多线程的一个特性:随机性。哪个线程被cpu执行,或者说抢到了cpu的执行权,哪个线程就执行。而cpu不会只执行一个,当执行一个一会后,又会去执行另一个,或者说另一个抢走了cpu的执行权。至于究竟是怎么样执行的,只能由cpu决定。

(7)Java中的多线程

          同其他大多数编程语言不同,Java内置支持多线程编程(Multithreaded Programming)。

          多线程程序包含两条或两条以上并发运行的部分,程序中每个这样的部分都叫做一个线程(Thread)。每个线程都有独立的执行路径,因此多线程是多任务处理的一种特殊形式。

         多任务处理被所有的现代操作系统所支持。然而,多任务处理有两种截然不同的类型:基于进程的和基于线程

        1. 基于进程的多任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。因此基于进程的多任务处理的特点是允许你的计算机同时运行两个或更多的程序。

         举例来说,基于进程的多任务处理使你在运用文本编辑器的时候可以同时运行Java编译器。

        在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。

        2. 而在基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。

        这意味着一个程序可以同时执行两个或者多个任务的功能。

2.Thread类介绍:

(1)线程的生命周期:

  a、新建状态(New):用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内存 
  b、就绪状态(Runnable):当一个线程创建了以后,其他的线程调用了它的start()方法,该线程就进入了就绪状态。处于这个状态的 线程位于可运行池中,等待获得CPU的使用权 
  c、运行状态(Running): 处于这个状态的线程占用CPU,执行程序的代码 
  d、阻塞状态(Blocked): 当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到 运行状态。 


(2)阻塞状态分为三种情况: 

  1)、 位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,java虚拟机就回把线程放到这个对象的等待池中 
  2)、 位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的琐池中。 

  3)、 其它的阻塞状态:当前线程执行了sleep()方法,或者调用了其它线程的join()方法,或者发出了I/O请求时,就会进入这个状态中。

• 创建并运行线程

       当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。

• 挂起和唤醒线程

       一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。suspend方法是不释放锁

虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成一些不可预料的事情发生,因此,这两个方法被标识为deprecated(弃用)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。

(3)终止线程的三种方法

    有三种方法可以使终止线程。
   a、 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 
   b、 使用stop方法强行终止线程(线程中调用了阻塞代码)(这个方法不推荐使用,因为stop是依靠抛出异常来结束线程的,也可能发生不可预料的结果)。如果没有调用阻塞代码,可以正常结束线程。

   c、使用interrupt方法中断线程(线程中调用了阻塞代码)(其实这种方法也是通过抛出异常来结束线程的)。如果没有调用阻塞代码,可以通过判断线程的中断标志位来介绍线程。


(4)线程的构造方法和方法:

a. 构造方法
public Thread(Runnable target)  分配新的 Thread 对象。(target - 其 run 方法被调用的对象。)
public Thread(Runnbale target,String name)分配新的 Thread 对象。(target - 其 run 方法被调用的对象,name - 新线程的名称。)
b.方法:
public final String getName():返回该线程的名称。
public final void setName(String name):改变线程名称,使之与参数 name 相同。 

public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 
public static Thread currentThread():返回当前正在执行的线程对象引用


▪ interrupt():中断线程,被中断线程会抛InterruptedException
▪ wait():等待获取锁:表示等待获取某个锁执行了该方法的线程释放对象的锁,JVM会把该线程放到对象的等待池中。该线程等待其它线程唤醒 
▪ notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。使线程由阻塞队列进入就绪状态
▪ sleep():让当前正在执行的线程休眠,有一个用法可以代替yield函数——sleep(0)

▪ 线程的暂停:public static void yield():暂停当前正在执行的线程对象,并执行其他线程。也就是交出CPU一段时间(其他同样的优先级或者更高优先级的线程可以获取到运行的机会)   例如:

//测试类代码:
     PriorityDemo pd = new PriorityDemo();

     Thread t1 = new Thread(pd);
     Thread t2 = new Thread(pd);

     t1.setName("林平之");
     t2.setName("岳不群");
     
     t1.start();
     t2.start();
		
     //线程的run方法:
     public void run() {
          for (int x = 0; x < 100; x++) {
                 System.out.println(Thread.currentThread().getName() + "---" + x);
                 // public static void yield()
                 Thread.yield();
           }
      }
▪ 线程的加入: public final void join()::等待此线程死亡后再继续,可使异步线程变为同步线程, join方法是不会释放锁

   一旦有join()线程,那么,当前线程必须等待,直到该线程结束。 例如:

PriorityDemo pd = new PriorityDemo();

     Thread t1 = new Thread(pd);
     Thread t2 = new Thread(pd);
     Thread t3 = new Thread(pd);

     t1.setName("林平之");
     t2.setName("岳不群");
     t3.setName("东方不败");

     t2.start();
     // join()线程 加入线程
     try {
             t2.join();
         } catch (InterruptedException e) {
			e.printStackTrace();
            }

                t1.start();
                t3.start();

    //注意:线程必须先启动才能join  否则不行
▪ 线程的守护:public final void setDaemon(boolean on):设置线程为守护线程,一旦前台(主线程),结束,守护线程就结束了。例如:

     DaemonDemo dd = new DaemonDemo();

     Thread t1 = new Thread(dd);
     Thread t2 = new Thread(dd);

     t1.setDaemon(true);
     t2.setDaemon(true);

     t1.start();
     t2.start();

     for (int x = 0; x < 10; x++) {
             System.out.println(Thread.currentThread().getName() + "---" + x);
     }

     注意:main方法本身也是一个线程
     守护线程举例:坦克大战  英雄联盟 很多游戏都有这样的规则

sleep和yield区别:
          1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。 
          2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。 
          3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常 

        4、sleep()方法比yield()方法具有更好的移植性 

注意:

           1)wait()和notify()方法是Object的,必须由锁对象调用
           2)wait()和notify() 必须写在同步代码块或者同步方法里面,也正是因为写在里面 才能有锁对象,才能让锁对象调用这两个方法
           3)wait()方法 让线程退出同步代码块等待 ,所以其他线程就可以进这个同步代码块了,(这也就是好多人常说的 释放锁对象,其实是退出到了同步代码块外面去了,所以其他线程可以进入了)

           4)notify()方法 唤醒线程队列中的随机一个处于等待状态的的线程(因为咱们这个只有t2线程等待,所以就肯定是唤醒的t2)
5)wait()和sleep(Long time)的区别(这个面试题非常容易考)
           wait():是Object类的方法,可以不用传递参数。释放锁对象。用锁对象来调用,需要锁对象调用notify来唤醒
           wait()必须写在同步代码块或者同步方法里面
           sleep():是Thread类的静态方法,需要传递参数。不释放锁对象。
           Thread直接调用sleep即可,自动睡眠一段时间,不需要唤醒,一段时间后自动睡醒一般写在run方法里面,(也可以写到其他任何地方,因为每个程序都是最起码有一个线程)

3.创建线程的方式

    方式1:继承Thread类。

              a、定义一个类继承Thread类。

              b、子类要重写Thread类的run()方法。

              c、让线程启动并执行。

        注意:调用start()方法,切记不是调用run方法这个方法,

        start()方法 其实做了两件事情,第一,让线程启动。第二,自动调用run()方法。

示例:

public class FileTest {
	public static void main(String[] args){
		MyThread my1 = new MyThread();
		MyThread my2 = new MyThread();
		my1.setName("用友");
		my2.setName("金蝶");
		my1.start();
		my2.start();
	}
}
class MyThread extends Thread {
	public void run() {
		for (int x = 0; x < 100; x++) {
			System.out.println(getName() + "---hello" + x);
		}
	}
}
运行结果:


    方式2:实现Runnable接口

             a、创建一个类实现Runnable接口

             b、重写run()方法

             c、创建类的实例

             d、把类的实例作为Thread的构造参数传递,创建Thread对象,让线程启动并执行

        注意:既然有了继承Thread类的方式,为什么还要有实现Runnable接口的方式?

                    1):避免的单继承的局限性

                    2):实现接口的方式,只创建了一个资源对象,更好的实现了数据和操作的分离。一般我们选择第二种方式。

示例:

public class FileTest {
	public static void main(String[] args) {
		MyRunnable my = new MyRunnable();
		// my.start();
		// 实现了Runnable接口的类没有start()方法,而我们启动线程必须调用start()方法。
		// 又由于,start()方法只有Thread类有。所以,我们就考虑这个把该类转换成Thread类。
		Thread t1 = new Thread(my);
		Thread t2 = new Thread(my);
		t1.setName("乔峰");
		t2.setName("慕容复");
		t1.start();
		t2.start();
	}
}
class MyRunnable implements Runnable {
	public void run() {
		for (int x = 0; x < 100; x++) {
			// getName()方法是Thread类的,而MyRunnable只实现了Runnable接口,本身没有getName(),所以不能使用。
			System.out.println(Thread.currentThread().getName() + "---hello"+ x);
		}
	}
}
运行结果:


4. Synchronized

synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行 它包括两种用法:synchronized 方法和 synchronized 块。

1. synchronized 方法:

  通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:

  public synchronized void accessVal(int newVal);

  synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

  在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

  synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

 

2. synchronized 块:

  通过 synchronized关键字来声明synchronized 块。语法如下:

  synchronized(syncObject) {
  //允许访问控制的代码
  }

  synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

        ▪对synchronized(this)的一些理解

  一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

  二、当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

  三、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。

  四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  五、以上规则对其它对象锁同样适用

       总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

在进一步阐述之前,我们需要明确几点:

A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

B.每个对象只有一个锁(lock)与之相关联。

C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

接着来讨论synchronized用到不同地方对代码产生的影响:

 

假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。

 

1. 把synchronized当作函数修饰符时,示例代码如下:

Public synchronized void methodAAA()
{
//….
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA()
{
synchronized (this)      // (1)
{
       //…..
}
}

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

2.同步块,示例代码如下:

public void method3(SomeObject so)
{
    synchronized(so)
    {
       //…..
    }
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable
{
        private byte[] lock = new byte[0]; // 特殊的instance变量
        Public void methodA()
        {
           synchronized(lock) { //… }
        }
        //…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3.将synchronized作用于static 函数,示例代码如下:

Class Foo
{
    public synchronized static void methodAAA()   // 同步的static 函数
    {
        //….
    }
    public void methodBBB()
    {
       synchronized(Foo.class)   // class literal(类名称字面常量)
    }
}

   代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

4.死锁问题:代码如下。

public void run() {
	if (flag) {
		synchronized (MyLock.objA) { 
			System.out.println("true -- objA");//d1--stop
			synchronized (MyLock.objB) { //d1
				System.out.println("true -- objB");
			}
		}
	} else {
		synchronized (MyLock.objB) {
			System.out.println("false -- objB");//d2
			synchronized (MyLock.objA) { //d2
				System.out.println("false -- objA");
			}
		}
	}
}

小结如下:

搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。

 

还有一些技巧可以让我们对共享资源的同步访问更加安全:

1. 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。

2. 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了

 

还有,比较常用的就有:Collections.synchronizedMap(new HashMap()),当然这个MAP就是生命在类中的全局变量,就是一个 线程安全的HashMap,web的application是全web容器公用的,所以要使用线程安全来保证数据的正确。

5. 线程的优先级:

(1). 试线程的优先级问题:线程默认优先级是5。范围是1-10。

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

      public final void setPriority(int newPriority):更改线程的优先级。

注意:优先级可以在一定的程度上,让线程获较多的执行机会。(效果不明显)

   Thread t1 = new Thread(pd);
   Thread t2 = new Thread(pd);
   hread t3 = new Thread(pd);

   t1.setName("林平之");
   t2.setName("岳不群");
   t3.setName("东方不败");
		
   t3.setPriority(10);
   t1.setPriority(1);
   t2.setPriority(1);

   t1.start();
   t2.start();
   t3.start();



6. 多线程经典案例卖票案例:

public class Test_4 {
	public static void main(String[] args) {
		ShouPiao sp=new ShouPiao();
		Thread t1=new Thread(sp);
		Thread t2=new Thread(sp);
		Thread t3=new Thread(sp);
		Thread t4=new Thread(sp);
		t1.setName("窗口1:");
		t2.setName("窗口2:");
		t3.setName("窗口3:");
		t4.setName("窗口4:");
		t1.start();
		t2.start();
		t3.start();
		t4.start();

/*		方式2:
 * 		ShouPiao sp1=new ShouPiao();
		ShouPiao sp2=new ShouPiao();
		ShouPiao sp3=new ShouPiao();
		ShouPiao sp4=new ShouPiao();
		sp1.setName("窗口1:");
		sp2.setName("窗口2:");
		sp3.setName("窗口3:");
		sp4.setName("窗口4:");
		sp1.start();
		sp2.start();
		sp3.start();
		sp4.start();*/

		
	}
}

class ShouPiao implements Runnable {
	int ticket = 100;
	public void run() {
		while (true) {
			synchronized (this) {
				try{
					Thread.sleep(50);
				}catch(Exception e){
					e.printStackTrace();
				}
				if (ticket > 0) {
					System.out.println(Thread.currentThread().getName() + "--"+ ticket--);
				} else {
					break;
				}
			}
		}
	}
}
/*
方式2:
class ShouPiao extends Thread {
	static int  ticket = 100;
	public void run() {
		while (true) {
			synchronized (this) {
				if (ticket > 0) {
					System.out.println(Thread.currentThread().getName() + "--"+ ticket--);
				} else {
					break;
				}
			}
		}
	}
}*/


 

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马程序员多线程练习题主要包括两个问题。第一个问题是如何控制四个线程在打印log之前能够同时开始等待1秒钟。一种解决思路是在线程的run方法中调用parseLog方法,并使用Thread.sleep方法让线程等待1秒钟。另一种解决思路是使用线程池,将线程数量固定为4个,并将每个调用parseLog方法的语句封装为一个Runnable对象,然后提交到线程池中。这样可以实现一秒钟打印4行日志,4秒钟打印16条日志的需求。 第二个问题是如何修改代码,使得几个线程调用TestDo.doSome(key, value)方法时,如果传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果。一种解决方法是使用synchronized关键字来实现线程的互斥排队输出。通过给TestDo.doSome方法添加synchronized关键字,可以确保同一时间只有一个线程能够执行该方法,从而实现线程的互斥输出。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [黑马程序员——多线程10:多线程相关练习](https://blog.csdn.net/axr1985lazy/article/details/48186039)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值