Java-多线程

时间:2021-04-12 22:25:37   收藏:0   阅读:27

一、概述

1、进程与线程

2、并发和并行

? 场景1:班级大扫除,下午4点的开始,小红在扫地,小明在擦窗,小李在擦黑板。下午4点15分结束

? 场景2:班级大扫除,下午4点开始,小红扫了15分钟地,然后擦了15分钟窗,最后擦了5分中黑板。下午4点35分结束。

? 总结:无论是场景1还是场景2,结果都是完成了班级大扫除,但是场景1的三件工作是同时执行的,这就是并行。而场景2是一件事结束然后快速切换到下一件事去执行,这便是并发。

技术分享图片

3、线程的状态

万事万物都不是凭空出现的,线程也一样,它被创建后的得状态被称为 新建 状态

比如:

Thread thread = new Thread();

线程被创建后是不能使用的,就是让用户在此期间设置一些属性

比如:

// 设置类加载器
thread.setContextClassLoader(System.class.getClassLoader());
// 设置线程名称
thread.setName("商品服务-product-service");
// 是否为守护线程/用户线程
thread.setDaemon(false);
// 设置线程优先级
thread.setPriority(5);

? 通过 thread.start() 方法开启线程,开启后意味着该线程 “能够” 运行,并不意味着一定会运行,因为它要抢占资源,获取CPU的使用权后,才能运行。所以此状态称为 可运行状态。

线程通过努力,获得了CPU的使用权,就会进入执行程序,此时状态被称为 运行状态。

多线程抢占CPU资源,同一时刻仅有一个线程进入临界区,为保证对资源访问的线程安全,同一时刻仅有一个线程进入 synchronized 同步块,而其他未获得访问权的线程将进入 阻塞状态 。

通过调用对象的wait(time)方法或调用线程的sleep(time)/join(time),等待/睡眠指定的时间,此时该线程会进入TIMED_WAITING(sleeping) 状态,直接时间已到,会进入Runnable状态,重新抢占CPU资源。

通过调用对象的wait()方法,让抢占资源的线程等待某工作的完成,或主动join()其他线程,让当前线程释放资源等待被join的线程完成工作,而该线程将进入 等待状态 。

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

小结:

线程状态有 5 种,新建,就绪,运行,阻塞,死亡

下面几个行为,会引起线程阻塞。

  • 主动调用 sleep 方法。时间到了会进入就绪状态
  • 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态
  • 调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。
  • 试图获取锁。成功的获取锁之后,会进入就绪状态。
  • 线程在等待某个通知。其它线程发出通知后,会进入就绪状态

4、单线程与多线程

5、程序执行原理(调度方式)

? 在操作系统中,有很多种调度方式,这里介绍分时调度和抢占式调度,JAVA中使用的是抢占式调度,所以主要介绍抢占式调度

6、主线程

? Java程序在执行过程中,先启动JVM,并加载对应的class文件,JVM会从main方法开始执行我们的程序代码,一直执行到main方法结束。这个步骤是有一个线程来执行的,这个线程就是主线程。当程序的主线程执行时,如果遇到了循环而导致程序在制定位置停留时间过程,则无法马上执行下面的程序,需要等待循环结束才能往后直行。那么能否实现一个主线程执行循环功能,另一个线程执行其他代码,最终实现多部分代码同时执行的效果呢?多线程便是解决这个问题的。

7、多线程内存理解

二、线程的创建

1、继承Thread创建线程

public class FirstThreadTest extends Thread {

    int i = 0;

    //重写run方法,run方法的方法体就是现场执行体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            //currentThread() 返回当前线程对象
            //getName 返回当前线程名字
            
            System.out.println(Thread.currentThread().getName() + "  : " + i);
            if (i == 50) {
                //创建线程对象并执行start()方法
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }


}

分析:

思考:为什么不直接创建Thread对象并start?

Thread t1 = new Thread;
t1.start();

答:以上代码没有语法错误,并不会报错,但是该线程进入运行状态时执行的run方法是Thread类当中的,Thread中的run方法并不是我们实际业务中需要的。Thread 类已经定义了线程任务的编写位置(run 方法),我们只需要继承Thread类并重写一个run方法足够了。

2、实现Runnable接口创建线程

Runnable 接口用来指定每一个线程要执行的任务,包含了一个 run 的无参数抽象方法,需要由接口实现重写该方法。此创建线程的方法是声明实现 Runnable 接口的类,该类实现 run 方法,然后创建 Runnable 的子类对象,传入到某个线程的构造方法中,开启线程

(1) 创建步骤:

定义实现类接口


//定义实现类接口
public class myRunnable implements Runnable {
    //重写run方法
    public void run()
    {
        for(int i = 0;i < 5;i++)
        {
            System.out.println("myRunnable线程正在执行!");
        }
    }

    
public static void main(String[] args)
{
    //创建线程执行目标类对象
    myRunnable mR = new myRunnable();
    //将Runnable接口的子类对象作为参数传递给Thread类的构造函数
    Thread t1 = new Thread(mR);
    Thread t2 = new Thread(mR);
    //开启线程
    t1.start();
    t2.start();
    for(int i = 0;i < 5;i++)
    {
        System.out.println("main线程正在执行!");
    }
}

}

3、线程的匿名内部类

使用线程的匿名内部类方式,可以方便的实现每个线程执行不同线程任务操作

方法一:重写 Thread 类中的方法创建线程


new Thread() {
    public void run() {
        for (int x = 0; x < 40; x++) {
            System.out.println(Thread.currentThread().getName()
                    + "...X...." + x);
        }
    }
}.start();

方法二:使用匿名内部类的方式实现 Runnable 接口,重写 Runnable 接口中的 run 方法


Runnable r = new Runnable() {
    public void run() {
        for (int x = 0; x < 40; x++) {
            System.out.println(Thread.currentThread().getName()
                    + "...Y...." + x);
        }
    }
};
new Thread(r).start();

JDK5.0以后新增的两种方式

4、通过Callable和Future创建线程

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

  1. call()可以返回值的。
  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

5、使用线程池

1.提供指定线程数量的线程池
2.执行指定的线程的操作(需要提供实现Runnable接口或Callable接口实现类的对象)
3.关闭连接池

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
    
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

说明:
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止

6、创建线程的三种方式对比

使用Runnable和Callable方式:

优势:

劣势:

1、编程变复杂了

使用Thread类创建线程方式:

优势:编程简单,易懂

劣势:只能单继承

三、常见的线程方法

  1. start():启动当前线程;调用当前线程的run()
  2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名字
  5. setName():设置当前线程的名字
  6. *yield():释放当前cpu的执行权
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  8. stop():已过时。当执行此方法时,强制结束当前线程。
  9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
  10. isAlive():判断当前线程是否存活

线程的优先级:

MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。

四、守护线程

? Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

? 如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

? 但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?

? 然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

? 答案是使用守护线程(Daemon Thread)

? 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

? 因此,JVM退出时,不必关心守护线程是否已结束。

? 如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

五:线程安全问题

1、出现的原因

? 当某个线程执行的过程中,尚未操作完成时,其他线程参与进来,产生了错误的数据。若让当前线程“睡眠(sleep(long millitime))”的时间越长,出现这类情况的概率往往越大。这就是线程安全问题。
例如在同一个电影院,有三个售票窗口同时卖同一场电影的票(每张电影票上会打印唯一的流水号,以显示卖的是第几张票),若三个售票窗口同时卖票,就有可能会出现同一个流水号的情况。

2、解决方法

在Java中,我们通过同步机制,来解决线程的安全问题。

2.1 方式一:同步代码块

synchronized()同步监视器{
    
}

说明:

  1. 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
    要求:多个线程必须要共用同一把锁。

2.2 方式二:同步方法

在实现多线程的方法中加上synchronized锁,如

private synchronized void show(){}

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

关于同步方法的总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  2. 非静态的同步方法,同步监视器是:this
    静态的同步方法,同步监视器是:当前类本身

2.3 方式三:Lock锁 — JDK5.0新增

1.实例化ReentrantLock
2.调用锁定方法lock()
3.调用解锁方法:unlock()
代码如下(示例):

class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}

2.4 synchronized 与 Lock的异同?

使用的优先顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)

使用同步方式的利弊:
利:同步的方式,解决了线程的安全问题。
弊:操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

3.死锁问题

3.1、可重入锁

? JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。

public class Counter {
    private int count = 0;

    public synchronized void add(int n) {
        if (n < 0) {
            dec(-n);
        } else {
            count += n;
        }
    }

    public synchronized void dec(int n) {
        count += n;
    }
}

? 观察synchronized修饰的add()方法,当知道到add()方法内部,当n<0的情况将去调用dec()方法,将会再去获取到this锁。

3.2 死锁

概念:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
我们使用同步时,要避免出现死锁。

例如:

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:

随后:

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。

那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

六、等待和唤醒机制

? 等待唤醒机制是为了方便处理进程之间通信的手段,多个线程在处理同一个资源时,由于处理的动作(线程的任务)不行同,为了使各个线程能够有效的利用资源,便采取了等待唤醒机制。等待唤醒机制涉及到的方法:

注:

代码实例:

来看一个例子,现有Person类,存储了姓名和年龄,使用 inPut 线程对 Person 类输入信息,使用 outPut 线程对 Person 类获取打印信息


//模拟Person类
public class Person {
    String name;
    int age;
    boolean flag = false;


//输入线程任务inPut类
public class inPut implements Runnable {
    private Person p;
    int count = 0;
    public inPut(Person p) {
        this.p = p;
    }
 
    public void run() {
        while (true)
        {
            synchronized (p)
            {
                if(p.flag)
                {
                    try {
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(count % 2 == 0)
                {
                    p.name = "儿童";
                    p.age = 3;
                }
                else
                {
                    p.name = "老人";
                    p.age = 99;
                }
                p.notify();
                p.flag = true;
            }
            count++;
        }
    }
}

//输出线程任务outPut类
public class outPut implements Runnable {
    private Person p;
    public outPut(Person p)
    {
        this.p = p;
    }
    public void run() {
        while (true)
        {
            synchronized (p)
            {
                if(!p.flag)
                {
                    try {
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(p.name + ":" + p.age + "岁");
                p.notify();
                p.flag = true;
            }
        }
    }
}


//在主线程中调用
public static void main(String[] args)
{
    Person P = new Person();
 
    inPut in = new inPut(P);
    outPut out = new outPut(P);
 
    Thread T1 = new Thread(in);
    Thread T2 = new Thread(out);
 
    T1.start();
    T2.start();

}

分析:

生产者消费问题:

生产者消费者问题是一个非常典型性的线程交互的问题。

  1. 使用来存放数据
    1.1 把栈改造为支持线程安全
    1.2 把栈的边界操作进行处理,当栈里的数据是0的时候,访问pull的线程就会等待。 当栈里的数据是200的时候,访问push的线程就会等待
  2. 提供一个生产者(Producer)线程类,生产随机大写字符压入到堆栈
  3. 提供一个消费者(Consumer)线程类,从堆栈中弹出字符并打印到控制台
  4. 提供一个测试类,使两个生产者和三个消费者线程同时运行,结果类似如下 :

栈类:

import java.util.ArrayList;
import java.util.LinkedList;
   
public class MyStack<T> {
   
    LinkedList<T> values = new LinkedList<T>();
       
    public synchronized void push(T t) {
        while(values.size()>=200){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        this.notifyAll();
        values.addLast(t);
         
    }
   
    public synchronized T pull() {
        while(values.isEmpty()){
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        this.notifyAll();
        return values.removeLast();
    }
   
    public T peek() {
        return values.getLast();
    }
}

生产者


 
public class ProducerThread extends Thread{
 
    private MyStack<Character> stack;
 
    public ProducerThread(MyStack<Character> stack,String name){
        super(name);
        this.stack =stack;
    }
     
    public void run(){
         
        while(true){
            char c = randomChar();
            System.out.println(this.getName()+" 压入: " + c);
            stack.push(c);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
    }
     
    public char randomChar(){
        return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
    }
     
}

消费者

public class ConsumerThread extends Thread{
 
    private MyStack<Character> stack;
 
    public ConsumerThread(MyStack<Character> stack,String name){
        super(name);
        this.stack =stack;
    }
     
    public void run(){
         
        while(true){
            char c = stack.pull();
            System.out.println(this.getName()+" 弹出: " + c);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         
    }
     
    public char randomChar(){
        return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
    }
     
}
public class TestThread {
       
    public static void main(String[] args) {
        MyStack<Character> stack = new MyStack<>();
        new ProducerThread(stack, "Producer1").start();
        new ProducerThread(stack, "Producer2").start();
        new ConsumerThread(stack, "Consumer1").start();
        new ConsumerThread(stack, "Consumer2").start();
        new ConsumerThread(stack, "Consumer3").start();
                           
    }
           
}

七、线程池:

7.1 什么是线程池

? 线程池是一种多线程处理形式,在多线程的场景下,频繁的创建线程和结束线程,会造成资源浪费,为了解决这个问题,引入线程池这种设计思想。

7.2、设计思路

线程池思路和生产者消费者模型很相似。

  1. 准备一个任务容器

  2. 一次性启动10个消费者线程

  3. 刚开始任务容器都是空的,所以线程都在wait

  4. 当外部线程往这个任务容器中扔了一个任务,就会有一个消费者线程被唤醒notify

  5. 这个消费者线程取出任务,并且执行这个任务,执行完毕后,继续等待下一次任务的到来

  6. 如果短时间内,有较多的任务进来,那么就有多个线程被唤醒,去执行这些任务。

    整个过程中,都不需要创建新的线程,而是循环使用已存在的线程

技术分享图片

package multiplethread;
  
import java.util.LinkedList;
  
public class ThreadPool {
  
    // 线程池大小
    int threadPoolSize;
  
    // 任务容器
    LinkedList<Runnable> tasks = new LinkedList<Runnable>();
  
    // 试图消费任务的线程
  
    public ThreadPool() {
        threadPoolSize = 10;
  
        // 启动10个任务消费者线程
        synchronized (tasks) {
            for (int i = 0; i < threadPoolSize; i++) {
                new TaskConsumeThread("任务消费者线程 " + i).start();
            }
        }
    }
  
    public void add(Runnable r) {
        synchronized (tasks) {
            tasks.add(r);
            // 唤醒等待的任务消费者线程
            tasks.notifyAll();
        }
    }
  
    class TaskConsumeThread extends Thread {
        public TaskConsumeThread(String name) {
            super(name);
        }
  
        Runnable task;
  
        public void run() {
            System.out.println("启动: " + this.getName());
            while (true) {
                synchronized (tasks) {
                    while (tasks.isEmpty()) {
                        try {
                            tasks.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    task = tasks.removeLast();
                    // 允许添加任务的线程可以继续添加任务
                    tasks.notifyAll();
  
                }
                System.out.println(this.getName() + " 获取到任务,并执行");
                task.run();
            }
        }
    }
  
}

八、原子性操作

概念:

所谓原子性操作就是不可中断的操作,比如赋值:int i=5;

原子性本身是线程安全的,但是i--这个行为是有三个原子性操作组成:

step1:取i的值

step2:i-1

step3:把新的值赋予i

这三步都是线程安全的,但是合在一起,就不安全了。比如:

当余票i只剩1张时i=1,有两个用户线程A,B来买票,

当A执行第一步和第二步的时候,还没将值i=0赋予i。

用户B取到了i的值i=1,这样结果会发生卖出了两张票的情况。

这就是产生线程安全问题的原理。

九:多线程高频面试问题

1、多线程有几种实现方案,分别是哪几种?

2、同步有几种方式,分别是什么,并分别说出其同步锁对象?

3、启动一个线程时 run() 还是 star() , 说说他们的区别?

4、sleep() 和 wait() 方法的区别?

5、为什么 wait()、notify()、notifyAll() 等方法都定义在 Object 类中

原文:https://www.cnblogs.com/feixiong1/p/14649365.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!