多线程学习(基础)

一、什么是多线程

程序:

是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象

进程:

是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有他自身的产生,存在和消亡的过程。运行中的QQ,运行中的播放器都是一个进程;程序是静态的,进程是动态的;进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程:

进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执多个线程,就是支持多线程的;线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小;一个进程中,多个线程共享相同的内存单元/内存地址空间,也就是说他们从同一堆中访问相同的变量和对象,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

jvm虚拟机结构

单核CPU和多核CPU的理解

单核CPU:其实就是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员,如果某个人不想交钱,那么收费员就可以把他"挂起"。但是因为CPU时间单元特别短,因此感觉不出来
如果是多核CPU才能更好的发挥多线程的效率。
一个java的程序java.exe,其实至少有三个线程,main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并行和并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀,多个人做同一件事

何时需要多线程

1)程序需要同时执行两个或多个任务
2)程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3)需要一些后台运行的程序时。

线程的调度

1)时间片:同优先级线程组成先进先出队列(先到先服务),使用时间片策略
2)抢占式:对高优先级,使用优先调度的抢占式策略

二、多线程的创建

1、继承Thread类

1)创建一个继承于Thread的类
2)重写Thread的run()方法
3)创建Thread类的子类对象
4)通过此对象调用Thread的start()方法
(start方法的作用:①启动当前线程,②调用当前线程的run)

2、实现Runnable接口

1)创建一个实现了Runnable的接口
2)实现类去实现run()方法
3)创建实现类对象
4)将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5)通过Thread类的对象调用start()

3、实现Callable接口

1)创建一个实现Callable的实现类
2)实现call()方法,将此线程需要执行的操作声明在call()中
3)创建Callable接口实现类的对象
4)将此Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask对象
5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6)获取Callable中call方法的返回值

4、使用线程池创建

背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用后放回池中,可以避免频繁创建销毁,实现重复利用,类似生活中的公共交通工具。
好处:提高响应速度(减少了创建新线程的时间);降低资源消耗(重复利用线程池中的线程,不需要每次都创建);便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止

5、Thread和Runnable参考示例如下

1)Thread
package com.xigu.concurrent;


/**
 * 1)创建一个继承于Thread的类
 * 2)重写Thread的run()方法
 * 3)创建Thread类的子类对象
 * 4)通过此对象调用Thread的start()方法
 * (start方法的作用:①启动当前线程,②调用当前线程的run)
 */
class ThreadDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("*************thread1**************" + i);
            }
        }
    }
}
public class ThreadTest extends Thread {
    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo();
        t1.start();
//       问题一:不能直接调用线程的run()方法,需要重新创建一个线程的对象
//       t1.run();
//       ThreadDemo t2 = new ThreadDemo();
//       t2.start();

        //如下操作仍然是在main线程中执行
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("***************main**************" + i);
            }
        }
    }
}
2)Runnable
package com.xigu.concurrent;

/**
 * 1)创建一个实现了Runnable的接口
 * 2)实现类去实现run()方法
 * 3)创建实现类对象
 * 4)将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
 * 5)通过Thread类的对象调用start()
 */

//创建一个实现了Runnable的接口
class MyRunnable implements Runnable {
    //实现类去实现run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("*************thread1**************" + i);
            }
        }
    }
}

public class RunnableTest {
    public static void main(String[] args) {
        //创建实现类对象
        MyRunnable myRunnable = new MyRunnable();
        //将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(myRunnable);
        //通过Thread类的对象调用start()
        t1.start();
    }
}
3)Callable
package com.xigu.concurrent;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 1)创建一个实现Callable的实现类
 * 2)实现call()方法,将此线程需要执行的操作声明在call()中
 * 3)创建Callable接口实现类的对象
 * 4)将此Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask对象
 * 5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
 * 6)获取Callable中call方法的返回值
 */

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


public class CallableTest {
    public static void main(String[] args) {
        //3)创建Callable接口实现类的对象
        MyCallable myCallable = new MyCallable();
        //4)将此Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask对象
        FutureTask futureTask = new FutureTask(myCallable);
        //5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread t1 = new Thread(futureTask);
        t1.start();
        try {
            //获取到Callable的返回值,get()方法一般放在最后面
            Object sum = futureTask.get();
            System.out.println("总和为" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
4)线程池
package com.xigu.concurrent;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

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

        //2)执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service1.execute(new ThreadDemo());//适用于Runnable
        service1.submit(new CallableDemo());//适用于Callable
        //3)关闭连接池
        service.shutdown();
    }
}

//创建一个实现了Runnable的接口
class RunableDemo implements Runnable {
    //实现类去实现run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("*************thread**************" + i);
            }
        }
    }
}

//创建一个实现了Callable的接口
class CallableDemo implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("*************callable**************" + i);
                sum += i;
            }
        }
        return sum;
    }
}

6、继承Thread和实现Runnable方法对比

开发中:优先选择,实现Runnable接口的方式
原因:
1、实现的方式没有类的单继承性的局限性
2、实现的方式更适合来处理多个线程有共享数据的情况
3、线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
联系:public class Thread implements Runnable(也就是说实际上Thread也是Runnable的一个实现类)
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

Runnable
Thread

上图对比原因是:MyThread创建了两个实例,自然会卖出两倍,属于用法错误

7、实现Runnable和实现Callable作对比

相比于Runnable,Callable方法更强大,相比run()方法,可以有返回值,方法可以抛出异常,支持泛型的返回值。需要借助于FutureTask类,比如获取返回结果时。

三、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():判断当前线程是否存活

sleep()、wait()、join()、yield()的区别

一、锁池和等待池维度
1.锁池:
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
2.等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
二、其他区别
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而wait则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程。

public static void main(String[] args) throws InterruptedException { 
  Thread t1 = new Thread(new Runnable() { 
  @Override
     public void run() { 
        try {
            Thread.sleep(3000); 
        } catch (InterruptedException e) {
            e.printStackTrace(); 
        }
        System.out.println("22222222");
       } 
  }); 
  t1.start();
  t1.join();   
  //这行代码必须要等t1全部执行完毕,才会执行         
  System.out.println("1111"); }

##输出结果
##22222222
##1111

四、线程的分类

java中线程分为两类:一种是守护线程,一种是用户线程
两者几乎是相同的,唯一的区别就是判断JVM何时离开。守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
java的垃圾回收就是一个典型的守护线程,若JVM中都是守护线程,当前JVM将退出。
形象理解:兔死狗烹、鸟尽弓藏

五、线程的生命周期

JDK中用Thread.state类定义了线程的几种状态,在他完整的生命周期中通常要经历以下五种状态:
1)新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
2)就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
3)运行:当就绪的线程被调度并获得到CPU资源时,便进入了运行状态,run()方法定义了线程的操作和功能。
4)阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
5)死亡:线程完成了他的全部工作或线程被提前强制性的中止或出现异常导致结束。

阻塞的情况又分为三种:
(1)、等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法。
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

线程的生命周期的切换

六、线程的同步问题

如下所示:假如原来有余额3000,有两个线程,都要对账户余额进行扣款2000,举例说首先要验证一下2000<3000,验证通过了,但是还没有进行下一步,这时候又来了一个线程,也是先验证一下,也通过了。然后两个线程都进行下一步扣款,这样就导致了,出现了-1000。

问题示例

6-1解决方法:

1、同步代码块

synchronized(同步监视器){
//需要被同步的代码
}

说明:
1、操作共享数据的代码,即为需要被同步的代码
2、共享数据:多个线程共同操作的变量
3、同步监视器:俗称锁,任何一个类的对象都可以充当锁,但是要保证多个线程必须共用一把锁(举例说一个公共厕所,假如说每个人都带一把锁的话那么其他人看不到究竟是否上锁)。
继承Thread的方式因为是要几个线程就会创建出几个对象,所以同步监视器不能用this(当前对象),而反观实现Runnable的方式,实际上只会有一个对象,所以,可以用this同步监视器。Thread可以用当前类.class。类也是对象,当前的类是一直不变的。

2、同步方法

同步方法就是类似于上面代码块,同样也需要把操作共享数据的代码拿出来,拿到一个方法里面,将方法用synchronized进行修饰。同步方法仍然要涉及到同步监视器,只是不再需要显示的声明。如果同步方法是非静态方法,默认的同步监视器就是this,如果是静态方法,默认的同步监视器是当前类本身。

3、Lock方法

从JDK5.0开始,java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

package com.xigu.concurrent;

import java.util.concurrent.locks.ReentrantLock;

public class Window implements Runnable {
    private ReentrantLock lock = new ReentrantLock();
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //调用锁定方法
            lock.lock();
            if (ticket > 0) {
                System.out.println("当前线程:" + Thread.currentThread().getName() + ",剩下票数:" + ticket);
                ticket--;
            } else {
                System.out.println("没有票了");
                break;
            }
            //调用解锁方法
            lock.unlock();
        }
    }
}

class test {
    public static void main(String[] args) {
        Window window = new Window();
        Thread t1 = new Thread(window);
        Thread t2 = new Thread(window);
        Thread t3 = new Thread(window);

        t1.setName("线程1");
        t1.setName("线程2");
        t1.setName("线程3");

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

问题:synchronized和lock的异同?

相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码后,自动释放同步监视器,lock需要手动开启同步lock(),同时结束同步需要手动的实现unlock()。synchronized有代码块锁和方法锁,lock只有代码锁。使用lock,jvm将花费更少的时间来调度线程。

4、优先使用顺序

Lock>同步代码块>同步方法

6-2带来的问题:

好处:同步的方式,解决了线程的安全问题。
坏处:操作同步代码时,只能有一个线程参与,其他线程等待,想当于是一个单线程的过程,效率低。还可能造成线程的死锁

线程的死锁问题
死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步

6-3、并发的三大特性

  • 原子性

原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。

image.png

1:将 count 从主存读到工作内存中的副本中
2:+1的运算
3:将结果写入工作内存
4:将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)

程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。关键字:synchronized

  • 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

image.png

如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。关键字:volatile、synchronized、final

  • 有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。

image.png

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。关键字:volatile、synchronized

volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由 “一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)

6-4、volatile

  1. 被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
image.png

2. 禁止指令重排序优化。

image.png

使用了volatile修饰之后就变得不一样了,不会出现上面有序性的那种问题。

  • 第一:使用volatile关键字会强制将修改的值立即写入主存;
  • 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  • 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。

七、线程通信

1、涉及的常见方法

wait():一旦进入此方法,当前线程就会进入阻塞状态,并释放同步监视器
notify():一旦进入此方法,就会唤醒wait的一个线程,如果多个线程wait,就会唤醒优先级高的线程
notifyAll():一旦进入此方法,就会唤醒所有wait线程

说明:

1、这三个方法必须在同步代码快或者同步方法中。
2、这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,否则会出现IlegalMonitorStateException异常
3、这三个方法是定义在Object类

面试题:wait和sleep方法的异同点?

相同点:一旦执行此方法,都可以使得当前线程进入阻塞状态
不同点:
1)两个方法声明的位置不同,Thread中声明sleep(),Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须在同步代码块、同步方法中使用。
3)关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep()不释放锁,wait()释放锁。

八、多线程8锁

  • 1、标准访问,先打印邮件还是先发送短信
  • 2、邮件方法暂停4秒钟,先打印邮件还是hello?
  • 3、新增一个普通方法hello(),先打印邮件还是hello()?
  • 4、两部手机,请问先答应邮件还是短信
  • 5、两个静态同步方法,同一部手机,先打印邮件还是短信
  • 6、两个静态同步方法,两部手机,先打印邮件还是短信
  • 7、一个普通同步方法,一个静态同步方法,一部手机,先邮件还是短信
  • 8、一个普通同步方法,一个静态同步方法,两部手机,先邮件还是短信
package com.xigu.concurrent;

class Phone {
    public synchronized void sendMail() {
        System.out.println(Thread.currentThread().getName() + ":sendMail");
    }

    public synchronized void sendEMS() {
        System.out.println(Thread.currentThread().getName() + ":sendEMS");
    }

    public void sayHello() {
        System.out.println(Thread.currentThread().getName() + ":hello");
    }

}

public class Lock8 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        //为了达到A先启动,B后启动的效果
        Thread.sleep(200);

        new Thread(() -> {
            try {
                phone1.sendEMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

1、先打印邮件,然后发送短信
一个对象里面如果有多个synchronized方法,某一时刻内,只要有一个线程调用了其中的一个synchronized方法,其他线程只能等待。换句话说,只能有唯一一个线程去访问这些synchronized方法。因为锁住的是对象this,被锁定后,其他线程都不能进入到当前对象的其他synchronized方法。
2、3、先hello,4s后再打印邮件
加个普通方法后发现和同步锁无关。
4、先打印SMS方法,大约4s后打印邮件方法。
A线程先启动,0.2s后B线程启动,A调用sendMail方法,然后陷入4s睡眠。B线程后来居上,打印SMS方法,然后打印邮件方法。因为换成两个对象后,不再是同一把锁了,就好比用两部手机,一部在发邮件,一部在发短信,互不影响。

public class Lock8 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        //为了达到A先启动,B后启动的效果
        Thread.sleep(200);

        new Thread(() -> {
            try {
                phone2.sendEMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

5、6、先睡眠大概4s,然后打印邮件,再发送短信
第五题和第六题都是一样的答案,因为静态方法的锁是类class,类被锁住了,下一个线程想要拿到锁还是得等到当前线程将锁释放才行。

package com.xigu.concurrent;


class Phone {
    public static synchronized void sendMail() throws Exception{
        Thread.sleep(4000);
        System.out.println(Thread.currentThread().getName() + ":sendMail");
    }

    public static synchronized void sendEMS() {
        System.out.println(Thread.currentThread().getName() + ":sendEMS");
    }

    public void sayHello() {
        System.out.println(Thread.currentThread().getName() + ":hello");
    }

}

public class Lock8 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        //为了达到A先启动,B后启动的效果
        Thread.sleep(200);

        new Thread(() -> {
            try {
                phone1.sendEMS();
                //phone2.sendEMS();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

7、8、先打印EMS,大概4s后打印mail()
弄懂了上面,就较为简单了,看锁的对象是什么,能不能拿到锁就行了

package com.xigu.concurrent;


class Phone {
    public synchronized void sendMail() throws Exception {
        Thread.sleep(4000);
        System.out.println(Thread.currentThread().getName() + ":sendMail");
    }

    public static synchronized void sendEMS() {
        System.out.println(Thread.currentThread().getName() + ":sendEMS");
    }

    public void sayHello() {
        System.out.println(Thread.currentThread().getName() + ":hello");
    }

}

public class Lock8 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        //为了达到A先启动,B后启动的效果
        Thread.sleep(200);

        new Thread(() -> {
            try {
                phone1.sendEMS();
                //phone2.sendEMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

总结:
synchronized实现同步的基础:java中的每一个对象都可以作为锁
具体有三种形式:
普通同步方法:锁的是当前实例对象
静态同步方法:锁的是当前类class的对象
同步方法块:锁的是synchronized括号里面配置的对象

如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态方法必须等待锁的方法释放锁才能获取到锁,但是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以不需要等该是对象已获取到锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的是同一把锁——类对象本身。如果一个是静态同步方法,一个是非静态同步方法,那么这两把锁是不同的,所以二者之间是不会有竞态条件的。但是一旦一个静态同步方法获取到锁之后,其他静态同步方法都必须要等该方法的锁释放后才能获取到锁,而不管他们是不是同一个实例对象。只要他们是同一个类的实例对象,就都要遵循!当一个线程试图访问同步代码块时,它首先必须获取到锁,退出或抛出异常时必须释放锁。

九、多线程练习

题目一:3个售票员卖出30张票

思路:线程一>操作(对外暴露的方法)一> 资源类
传统写法:线程,操作(saleTicket方法),资源类(Ticket )

class Ticket {

    private int ticket = 30;

    public synchronized void saleTicket() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖掉了一张票,还剩" + ticket--);
        }
    }
}

public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //Thread的构造方法,Thread(Runnable target,String name)
        //如下传入一个匿名内部类+name
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "售票员C").start();
    }
}

新写法:使用了LambdaExpress的写法

class Ticket {

    private int ticket = 100;
    private Lock lock = new ReentrantLock();
    public void saleTicket() {
        lock.lock();
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖掉了一张票,还剩" + ticket--);
        }
        lock.unlock();
    }
}

public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "售票员A").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "售票员B").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "售票员C").start();
    }
}

题目二:生产者消费者问题,原始值为0,一个加1一个减1,来10轮

思路:判断一>干活一>通知
注意:永远在while{}中使用wait()方法

class Product {
    private int number = 0;

    //生产
    public synchronized void produce() throws InterruptedException {
        //判断
        while (number != 0) {
            this.wait();
        }
        //干活
        number++;
        System.out.println(Thread.currentThread().getName() + "生产了一件商品,当前剩余商品" + number);
        //通知
        this.notifyAll();
    }


    //消费
    public synchronized void reduce() throws Exception {
        //判断
        while (number == 0) {
            this.wait();
        }
        //干活
        number--;
        System.out.println(Thread.currentThread().getName() + "消费了一件商品,当前剩余商品" + number);
        //通知
        this.notifyAll();
    }
}

public class AirCondition {
    public static void main(String[] args) {

        Product product = new Product();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + "来了");
                    product.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + "来了");
                    product.reduce();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "消费者B").start();
    }
}

题目三:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品
分析:
1、是否是多线程问题?是,生产者线程,消费者线程
2、是否有共享数据?是,店员/产品
3、如何解决线程安全问题?同步机制,三种方法。
4.是否涉及多线程的通信?是

class Product {
    private int number = 0;

    //生产
    public synchronized void Productor() throws InterruptedException {
        //判断
        while (number >= 20) {
            System.out.println("店员通知停止生产");
            this.wait();
        }
        //执行
        number++;
        System.out.println(Thread.currentThread().getName() + "生产了商品,还剩" + number);
        //通知
        this.notifyAll();
    }

    //消费
    public synchronized void Customer() throws InterruptedException {
        //判断
        while (number == 0) {
            this.notifyAll();
            System.out.println("店员通知开始生产");
            this.wait();
        }
        //执行
        number--;
        System.out.println(Thread.currentThread().getName() + "消费了商品,还剩" + number);
    }
}

public class ProductorAndCustomer {
    public static void main(String[] args) {
        Product product = new Product();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    product.Productor();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者").start();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    product.Customer();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者A").start();

    }
}

你可能感兴趣的:(多线程学习(基础))