Java高级编程(一)——多线程

多线程

目录

多线程

概念

创建多线程的方式

方式一 Tread类

方式二 Runnable接口

方式三 Callable接口

方式四 线程池

线程的生命周期

线程的同步

方式一 同步代码块

方式二 同步方法

单例模式的线程安全

死锁

方式三 Lock锁

线程的通信

sleep()和wait()

生产者/消费者问题

概念

区分各个概念

  • 程序:一段静态的代码

  • 进程:程序的一次执行过程或正在运行的一个程序,是一个动态过程,有生命周期

    • 进程是资源分配的单位,系统在运行时为每个进程分配不同的内存区域

  • 线程:进程可以进一步细化成线程,是一个程序内部的执行路径

    • 多线程:同一时间可以执行多个线程

    • 线程是调度和执行的最小单位,每个线程有独立的运行栈和程序计数器pc

    • 一个进程中的多个线程共享相同的内存单元,他们从同一堆中分配对象,可以访问相同的变量和对象,共享方法区和堆。但是多个线程操作共享的系统资源可能会带来安全隐患

 

  • 单核CPU与多核CPU:单核CPU是假的多线程,一个时间单元内还是只能执行一个线程的任务。java的一个程序至少有三个线程,main()主线程、gc()垃圾回收线程、异常处理线程,发送异常会影响主线程

  • 并发与并行

    • 并行:多个CPU同时执行多个任务

    • 并发:一个CPU(采用时间片)同时执行多个任务

需要多线程的情况

  • 程序需要同时执行多个程序

  • 程序需要实现一些需要等待的任务时,比如用户输入、文件读写操作、网络操作、搜索等

  • 需要一些后台程序时

创建多线程的方式

方式一 Tread类

/* 创建一个继承于Thread类的子类
* 重写Thread类的run(),将此线程执行的操作声明在run中
* 创建Thread类的子类对象
* 通过此对象调用start()*/
class Mythread extends Thread{
    @Override
    public void run(){
        for (int i=0;i<100;i++){
            if(i%2==0) System.out.println(Thread.currentThread().getName()+":"i);//打印当前线程名
        }
    }
}
public class ThreadTest{
    public static void main(String[] args) {
        Mythread t1=new Mythread();
        t1.start();//启动当前线程+调用当前线程的run方法
        for (int i=0;i<100;i++){
            if(i%2==0) System.out.println(Thread.currentThread().getName()+":***main***");
        }
    }
}//结果是两个循环的输出相互交叉,因为是两个线程在跑互不干扰
  • start()的时候应该调父类的run方法,但是子类重写了,因此调用的是重写的这个run()。

  • 对于一个线程,只能start一次,对于一个已经start过的线程不能再通过start起一个新的线程

常用的方法:

void start();//启动线程并执行对象的run()方法
run();//线程在被调度时执行的操作
String getName();//返回线程的名称
void setName(String name);//设置该线程名称
static Tread currentTread();//返回当前线程,在Thread子类中就是this,通常用于主线程和Runable实现类
static void yield();//释放当前CPU的执行,释放后各线程竞争
join();//插队,会把插入的线程执行完了才会执行刚刚的线程
boolean isAlive();//判断线程是否还活着
sleep(long millitime);

线程调度

  • 对同优先级线程组成先进先出队列,使用时间片策略

  • 对高优先级,使用优先调度的抢占式策略

线程的优先级:一般就10级,默认为5

  • MAX_PRIORITY:10

  • MIN_PRIORITY:1

  • NORM_PRIORITY:5

 getPriority();//返回线程的优先级
 setPriority(int newPriority);//改变线程的优先级

线程创建时继承父线程的优先级,低优先级知识获得调度的概率低,不是一定是在高优先级线程之后才被调用

多线程问题

 class Window extends Thread{
     //private int ticket=50; 不行,会造成三个线程各执行各的,出现三个ticket总共150个
     private static int ticket=50; //加上static后三个线程共用一个ticket,会产生3个50,并发问题。ticket是共享数据
     @Override
     public void run(){
         while(true){
             if(ticket>0){
                 System.out.println(getName()+":票号为:"+ticket);
                 ticket--;
             }else{
                 break;
             }
         }
     }
 }
 public class ThreadTest{
     public static void main(String[] args) {
         Window t1=new Window();
         Window t2=new Window();
         Window t3=new Window();
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");
         t1.start();
         t2.start();
         t3.start();
     }
 }
  • 显示可能不按顺序,因为显示也需要时间

Thread类本身也实现了Runnable接口

方式二 Runnable接口

 /*创建一个实现Runnable接口的类
 * 实现类去实现Runnable中的抽象方法run()
 * 创建实现类的对象,将此对象作为参数传递到Tread类的构造器中
 * 创建Tread类对象,通过Tread类的对象调用start*/
 class MThread implements Runnable{
     @Override
     public void run() {
         for (int i=0;i<50;i++){
             System.out.println(i);
         }
     }
 }
 public class RunnableTest {
     public static void main(String[] args) {
         MThread m1=new MThread();
         Thread t1=new Thread(m1);
         t1.start();//调用当前线程的run()-->调用了Runnable类型的target的run()
     }
 }

和上面一样,存在安全问题

 class Window implements Runnable{
     private int ticket=50; //可以不加static
     @Override
     public void run(){
         while(true){
             if(ticket>0){
                 System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
                 ticket--;
             }else{
                 break;
             }
         }
     }
 }
 public class ThreadTest{
     public static void main(String[] args) {
         Window w=new Window();
         Thread t1=new Thread(w);
         Thread t2=new Thread(w);
         Thread t3=new Thread(w);
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");
         t1.start();
         t2.start();
         t3.start();
     }
 }

开发中,优先选择实现Runnable接口的方式。因为该方式没有类的单继承的局限性,实现的方式更适合来处理多个线程有共享数据的情况

方式三 Callable接口

JDK5.0新增

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

FutureTask,借助了Future接口

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等

  • FutureTast是Future接口唯一的实现类

  • FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值

 class NumThread implements Callable{//创建一个实现Callable接口的实现类
     int sum =0;
     @Override
     public Object call() throws Exception {//实现call方法,将此线程需要执行的操作声明在call方法中
         for (int i =0; i < 50; i++){
             if(i % 2 == 0){
                 System.out.println(i);
                 sum+=i;
             }
         }
         return sum;
     }
 }
 ​
 public class ThreadNew1 {
     public static void main(String[] args) {//创建Callable接口实现类的对象
         NumThread numThread=new NumThread();
         FutureTask futureTask=new FutureTask(numThread);
         //将此Callable接口实现类的对象作为参数传递到FutureTask构造器中
         new Thread(futureTask).start();
         //将FutureTasj对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
         try {//需要则获取Callable中call方法的返回值
             Object sum=futureTask.get();
             //get()的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
             System.out.println("总和为"+sum);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
     }
 }
  • call()可以有返回值。可以抛出异常,被外面的操作捕获,获取异常信息

  • Callable支持泛型

方式四 线程池

提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用

相关API

  • ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor

  • Executors:工具类,用于创建并返回不同类型的线程池

线程池提高响应速度(减少了创建线程的时间),降低资源消耗(重复利用线程),便于线程管理

  • corePoolSize:核心池的大小

  • maximumPoolSize:最大线程数

  • keepAliveTime:线程没有任务时最多保持多长时间后终止

 class NumberThread implements Runnable{
 ​
     @Override
     public void run() {
         for (int i =0; i < 50; i++){
             if(i % 2 == 0) System.out.println(Thread.currentThread().getName()+":"+i);
         }
     }
 }
 public class ThreadNew1 {
     public static void main(String[] args) {
         ExecutorService service = Executors.newFixedThreadPool(10);//提供指定数量的线程池
         service.execute(new NumberThread());//适用于Runnable
         //service.submit();适用于Callable
         service.shutdown();//关闭线程池
     }
 }
 //System.out.println(service.getClass());得在接口的实现类,利用sevice.getClass()获得实现类名称
 ThreadPoolExecutor service1= (ThreadPoolExecutor) service;
 service1.setCorePoolSize(10);//就可以设置各种属性

线程的生命周期

一个完整的生命周期

  • 新建

  • 就绪:start()后,进入线程队列等待CPU时间片,此时已经具备运行条件但是没分配到PCU资源

  • 运行:获得CPU资源,run()方法定义了线程的操作和功能

  • 阻塞:在特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止执行

  • 死亡:完成工作或被提前强制中止或出现异常导致结束

 

线程的同步

多个线程的执行的不确定性引起执行结果的不稳定,多个线程对账本的共享会造成操作的不完整性,破坏数据

方式一 同步代码块

 synchronized(同步监视器){
     //需要被同步的代码,即操作共享数据的代码
 }
  • 同步监视器,就是锁,任何一个类的对象都可以充当锁。但是要求多个线程必须要共用同一把锁,注意代码位置,是不是同一把锁!

  • 但是执行速度会变慢。加锁后外面是多线程并行,但是操作同步代码时只能有一个线程参与,相当于单线程

改进上述代码

 class Window implements Runnable{
     private int ticket=50;
     Object obj=new Object();
     @Override
     public void run(){
         //Object obj=new Object();如果放到这里,就不是1个了,每跑一次都有一个锁
         while(true){
             synchronized (obj) {
                 if(ticket>0){
                     try {//利用sleep放大出错的可能性
                         Thread.sleep(100);//保证了即使线程1在次阻塞了,其他线程也跟着等
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
                     ticket--;
                 }else{
                     break;
                 }
             }
         }
     }
 }//修复了错票、重票、负票等问题
 public class ThreadTest{
     public static void main(String[] args) {
         Window w=new Window();
         Thread t1=new Thread(w);
         Thread t2=new Thread(w);
         Thread t3=new Thread(w);
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");
         t1.start();
         t2.start();
         t3.start();
     }
 }

同一个obj,可以用static

 class Window extends Thread{
     private static int ticket=50;
     private static Object obj=new Object();//不用static,多个线程拿的就不是一个锁
     @Override
     public void run(){
         while(true){
             synchronized (obj) {
                 if(ticket>0){
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
                     ticket--;
                 }else{
                     break;
                 }
             }
         }
 ​
     }
 }
 public class ThreadTest{
     public static void main(String[] args) {
         Window t1=new Window();
         Window t2=new Window();
         Window t3=new Window();
         t1.setName("窗口1");
         t2.setName("窗口2");
         t3.setName("窗口3");
         t1.start();
         t2.start();
         t3.start();
     }
 }

可以用当前对象来代替

class Window implements Runnable{
    ...
    synchronized (this) {......}//this是Window类型的对象w,其他Thread类型全是从w出来的
    ...
}
public class ThreadTest{...}
class Window extends Thread{
    ...
    //synchronized (this) {......}不对!因为三个线程对应三个Window对象,this代表t1、t2、t3三个对象
    synchronized(window.class);//拿当前类充当对象 Class xxx=Window.class,这个是唯一的,因为类只会加载一次
    ...
}

synchronized如果包多了,比如包上while了,就导致一个线程拿着这个锁一直跑while

方式二 同步方法

如果操作共享数据的代码完整的声明在一个方法中,即可以将此方法声明为同步的

可以将上述while内的代码拎出来写一个完整方法(while不能包进去)

 
//class Window implements Runnable{}
 public synchronized void show(){
     if(ticket>0){
         try {
             Thread.sleep(100);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
         ticket--;
     }
 }
 public static synchronized void show(){//加上static即可,不然没用,此时同步监视器是Window.class
     ...
 }

同步方法仍然涉及到同步监视器,只是不需要显示的声明。

非静态的同步方法,同步监视器是this。非静态的同步方法,同步监视器是当前类本身

单例模式的线程安全

 class Bank {//单例模式
     private Bank(){}//构造器
     private static Bank insatnce=null;
     public static Bank getInsatnce(){//懒汉式
         if(insatnce==null){
             insatnce=new Bank();//这两行对同一对象操作,一个判断一个赋值,会有线程安全问题,需要synchronized
         }
         return insatnce;
     }
 }
 public static synchronized Bank getInsatnce(){...}//加一个synchronized,或者拿它把if包住

但是这样效率低,因为一个进入后其他线程全需要等待,可以做二重判断

 class Bank {//单例模式
     private Bank(){}//构造器
     private static Bank insatnce=null;
     public static Bank getInsatnce(){//懒汉式
         if(insatnce==null){
             synchronized (Bank.class) {
                 if(insatnce==null){
                     insatnce=new Bank();
                 }
             }
         }
         return insatnce;
     }
 }

死锁

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

解决方法:专门的算法、原则,尽量减少同步资源的定义,尽量避免嵌套同步

死锁的实例

 public class ThreadTest{
     public static void main(String[] args) {
         StringBuffer s1=new StringBuffer();
         StringBuffer s2=new StringBuffer();
 ​
         new Thread(){//匿名方式创建线程
             @Override
             public void run() {
                 synchronized (s1){//先拿锁s1再拿锁s2
                     s1.append("a");
                     s2.append("1");
                     try {
                         Thread.sleep(100);//利用sleep加大概率
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     synchronized (s2){
                         s1.append("b");
                         s2.append("2");
                         System.out.println(s1);
                         System.out.println(s2);
                     }
                 }
 ​
             }
         }.start();
 ​
         new Thread(new Runnable() {//匿名方式通过接口创建线程
             @Override
             public void run() {
                 synchronized (s2){//先拿锁锁s2再拿锁s1
                     s1.append("c");
                     s2.append("3");
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     synchronized (s1){
                         s1.append("d");
                         s2.append("4");
                         System.out.println(s1);
                         System.out.println(s2);
                     }
                 }
             }
         }).start();
     }
 }//程序运行后有几率锁死,无法继续运行下去

方式三 Lock锁

JDK5.0加入,通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当

上述抢票代码做修改

 class Window implements Runnable{
     private int ticket=50;
     private ReentrantLock lock=new ReentrantLock();//声明一个ReentrantLock类的对象
     @Override
     public void run() {
         while (true){
             try {
                 lock.lock();//调用lock(),下面try内的代码就类似同步代码块内,单线程
                 if(ticket>0){
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName()+":票号为:"+ticket);
                     ticket--;
                 }else break;
             }finally {
                 lock.unlock();//解锁
             }
         }
     }
 }
  • 如果是继承的方式创建多线程,记得给lock定义的时候加个static

synchronized和Lock

  • 都是解决线程安全的问题

  • Lock需要手动的启动同步(lock()方法),结束同步(unlock()方法)也要手动实现。synchronized机制是在执行完相应的同步代码以后,自动释放同步监视器

  • Lock只有代码块锁,synchronized有代码块锁和方法锁。但是使用Lock锁JVM花费更少的时间来调度线程,性能更好,且扩展性更好(有更多的子类)

线程的通信

交错打印

 class Number implements Runnable{
     private int number=1;
     @Override
     public void run() {
         while (true){
             synchronized (this) {
                 notify();//notify和notifyAll在此处都一样,因为只睡了一个。
                 //后一个线程执行到此处时唤醒上一个被wait的线程,但是后一线程拿着这个锁,从而实现交错
                 if (number <= 30){
                     System.out.println(Thread.currentThread().getName()+":"+number);
                     number++;
                     try {
                         wait();//使得调用如下wait方法的线程进入阻塞状态
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }else break;
             }
         }
     }
 }
 public class CommunicationTest {
     public static void main(String[] args) {
         Number number = new Number();
         Thread t1 = new Thread(number);
         Thread t2 = new Thread(number);
         t1.setName("线程1");
         t2.setName("线程2");
         t1.start();
         t2.start();
     }
 }
  • wait():一旦执行次方法,当前线程会进入阻塞状态,并释放同步监视器

  • notify():一旦执行,会唤醒被wait的一个线程,如果有多个线程wait,就唤醒优先级高的线程

  • notifyAll():一旦执行会唤醒所有被wait的线程

  • 线程通信中:上面三个方法的必须使用在同步代码块或同步方法中,调用者必须是同一个同步监视器

 Object obj=new Object();
 ...
     synchronized (obj) {
         this.notify();//和synchronized不是一个同步监视器,会报错:IllegalMonitorStateException
         ...

三个方法是定义在java.lang.Object类中

sleep()和wait()

都让线程进入阻塞状态

不同L

  • 声明位置不同:Thread类中声明sleep(),Object类中声明wait()

  • 调用范围不同:sleep()可以在任何场景下调用,而wait()只能在同步代码块或同步方法中使用

  • 对于同步监视器:如果二者都使用在同步代码块/同步方法中,sleep()不会释放锁,wait()会释放锁

生产者/消费者问题

 class Clerk{
     private int productCount=0;
     public synchronized void produceProduct() {
         if(productCount<20){
             productCount++;
             System.out.println(Thread.currentThread().getName()
                     +":生产第"+productCount+"个产品");
             notify();
         }else{
             try {
                 wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
     public synchronized void consumeProduct() {
         if(productCount>0){
             System.out.println(Thread.currentThread().getName()
                     +":消费第"+productCount+"个产品");
             productCount--;
             notify();
         }else{
             try {
                 wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 }
 ​
 class Producer extends Thread{
     private Clerk clerk;
     public Producer(Clerk clerk){
         this.clerk=clerk;
     }
     @Override
     public void run() {
         System.out.println(getName()+":生产产品");
         while (true){
             try {
                 sleep(4);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             clerk.produceProduct();
         }
     }
 }
 ​
 class Consumer extends Thread{
     private Clerk clerk;
     public Consumer(Clerk clerk){
         this.clerk=clerk;
     }
     @Override
     public void run() {
         System.out.println(getName()+":消费产品");
         while (true){
             try {
                 sleep(10);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             clerk.consumeProduct();
         }
     }
 }
 ​
 public class ProductTest {
     public static void main(String[] args) {
         Clerk clerk=new Clerk();
         Producer p1=new Producer(clerk);
         p1.setName("生产者");
         Consumer c1=new Consumer(clerk);
         c1.setName("消费者1");
         Consumer c2=new Consumer(clerk);
         c2.setName("消费者2");
 ​
         p1.start();
         c1.start();
         c2.start();
     }
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值