Java 多线程

多线程

多线程能使程序高效率利用CPU资源。



进程与线程

​ 进程:正在进行中的程序(直译)。
​ 线程:进程中一个负责程序执行的控制单元(也叫执行路径)。

进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。

1、一个进程中可以有多个执行路径,称之为多线程。每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。

2、一个进程中至少要有一个线程。

3、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。

多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多,会导致效率的降低。

其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。


JVM启动时至少有两个线程启动:

    1. 执行main函数的线程,该线程的任务代码都定义在main函数中。
    2. 负责垃圾回收的线程。



创建线程的方式

在 Java 中,创建线程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。


1.继承Thread,复写run方法
    1. 定义一个类继承Thread类。
    2. 覆盖Thread类中的run方法。
    3. 直接创建Thread的子类对象创建线程。
    4. 调用start方法开启线程并调用线程的任务run方法执行。

单线程示例:

class Demo{
      private String name ;
      Demo(String name){
             this.name = name;
      }
       public void show(){
             for(int x = 0; x < 10; x++){
                  System.out.println(name + "...x=" + x);
            }
      }
}

class ThreadDemo{
       public static void main(String[] args){
            Demo d1 = new Demo("旺财");
            Demo d2 = new Demo("小强");
            d1.show();
            d2.show();
      }
}

在单线程程序中,只有上一句代码执行完,下一句代码才有执行的机会。

创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。


jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在Thread类中的run方法。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。

多线程示例

class Demo extends Thread{
      private String name ;
      Demo(String name){
             this.name = name;
      }
       public void run(){
             for(int x = 0; x < 10; x++){
                  System.out.println(name + "...x=" + x + "...ThreadName=" + Thread.currentThread ().getName());
            }
      }
}

class ThreadDemo{
       public static void main(String[] args){
            Demo d1 = new Demo("旺财");
            Demo d2 = new Demo("xiaoqiang");
            d1.start(); //开启线程,调用run方法。
            d2.start();
            for(int x = 0; x < 20; x++){
                  System.out.println("x = " + x + "...over..." + Thread.currentThread().getName());
            }
      }
}


2.实现一个 Runnable 接口
    1. 定义类实现Runnable接口。
    2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
    3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
    4. 调用线程对象的start方法开启线程


实现Runnable接口的好处:

    1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
    2. 避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。

示例

//准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
//通过接口的形式完成。
class Demo implements Runnable{
      public void run(){
            show();
      }
      public void show(){
             for(int x = 0; x < 20; x++){
                  System.out.println(Thread.currentThread().getName() + "..." + x);
             }
      }
}

class ThreadDemo{
       public static void main(String[] args){
            Demo d = new Demo();
            Thread t1 = new Thread(d);
            Thread t2 = new Thread(d);
            t1.start();
            t2.start();
       }
}



线程安全

多线程很大程度上提高了程序的执行效率,但与此同时,也带来了安全问题。


线程安全问题产生的原因

​ 1. 多个线程在操作共享的数据。

​ 2. 操作共享数据的线程代码有多条。

​ 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。


线程安全问题的解决方案

将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

方案1: 同步代码

在java中,用同步代码块就可以解决这个问题。

/*
同步代码块的格式:   
    synchronized(对象){
          // 需要被同步的代码;
    } 
*/

class Ticket implements Runnable{
      private int num = 100;
      Object obj = new Object();

      public void run(){
             while(true ){
                   synchronized(obj ){//Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。
                         if(num > 0){
                              System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
                        }
                   } 
             }
      }
}

class TicketDemo{
       public static void main(String[] args){
            Ticket t = new Ticket();
            Thread t1 = new Thread(t);
            Thread t2 = new Thread(t);
            Thread t3 = new Thread(t);
            Thread t4 = new Thread(t);

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

同步的好处:解决了线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

同步的前提:必须有多个线程并使用同一个锁。


方案2:同步函数

在函数上加上 synchronized 修饰符也同样可以达到效果。

class Bank{
       private int sum ;
       public synchronized void add(int num){ //同步函数
             sum = sum + num;
             System.out.println("sum = " + sum);
       }
}


同步函数和同步代码块的区别:
​ 1. 同步函数的锁是固定的this。
​ 2. 同步代码块的锁是任意的对象。

静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。

多线程下的单例模式示例

//恶汉式,不存在安全问题,因为不存在多个线程共同操作数据的情况
class OneSingle{
       private static final OneSingle s = new OneSingle();
       private OneSingle(){}
       public static OneSingle getInstance(){
             return s ;
      }
}

//懒汉式,存在安全问题,可以使用同步函数解决
class Single{
      private static Single s = null;
       private Single(){} 
       public static Single getInstance(){
             if(s ==null){
                   synchronized(Single.class){//静态函数需要所属字节码文件对象
                         if(s == null)
                               s = new Single();
                  }
            }
            return s ;
      }
}


死锁

在解决线程安全的情况下,又带来一个新的问题,那就是死锁。

同步嵌套很容易产生死锁。

class Ticket implements Runnable{
       private static int num = 100;
       Object obj = new Object();
       boolean flag = true;

       public void run(){
             if(flag ){
                   while(true ){
                         synchronized(obj ){//t1线程 拿到 obj 的锁后,执行show 方法
                              show();
                        }
                  }
            } else
                   while(true )
                        show(); //t2 线程执行
      }

        // 此处是两线程同步处,此时t1线程已经拿到 obj的锁,t2则还没有锁
       public synchronized void show(){
            //此时,如果t2线程进来,表示t2 拿到了 this 锁,在这一步又需要t1持有的 obj  的锁,就会死锁
             synchronized(obj ){ 
                   if(num > 0){
                         try{
                              Thread. sleep(10);
                        } catch(InterruptedException e){
                              e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "...function..." + num--);
                  }
            }
      }
}

class DeadLockDemo{
       public static void main(String[] args){
            Ticket t = new Ticket();
            Thread t1 = new Thread(t);
            Thread t2 = new Thread(t);

            t1.start();
             try{
                  Thread. sleep(10);
            } catch(InterruptedException e){
                  e.printStackTrace();
            }
            t. flag = false ;
            t2.start();
      }
} 



等待唤醒机制

多个线程在处理统一资源,但是任务却不同,这时候就需要线程间通信。

在 Java 中 等待/唤醒 机制 就是处理线程间通信的。

等待/唤醒机制涉及的方法:

    1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
    2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
    3. notifyAll():唤醒线程池中的所有线程。

注意

  1. 这些方法都需要定义在同步中,因为这些方法是用于操作线程状态的方法。
  2. 方法必须要标示所属的锁。
  3. 这三个方法都定义在 Object 类中。这三个方法都需要定义在同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在 Object 类中。


wait 和 sleep 区别
​ 1)wait可以指定时间也可以不指定。sleep必须指定时间。
​ 2)在同步中时,对CPU的执行权和锁的处理不同。
​ wait:释放执行权,释放锁。
​ sleep:释放执行权,不释放锁。


生产者消费者示例

class Resource{
       private String name ;
       private int count = 1;
       private boolean flag = false;

       public synchronized void set(String name){
            while(flag )
                   try{
                         this.wait();
                  } catch(InterruptedException e){
                        e.printStackTrace();
                  }
             this.name = name + count;
             count++;
             System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
             flag = true ;
             notifyAll();
      }

       public synchronized void out(){
             while(!flag )
                   try{
                         this.wait();
                  } catch(InterruptedException e){
                        e.printStackTrace();
                  }
            flag = false ;
            notifyAll();
            System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
      }
}

class Producer implements Runnable{
       private Resource r ;
      Producer(Resource r){
             this.r = r;
      }
       public void run(){
             while(true ){
                   r.set( "烤鸭");
            }
      }
}

class Consumer implements Runnable{
       private Resource r ;
      Consumer(Resource r){
             this.r = r;
      }
       public void run(){
             while(true ){
                   r.out();
            }
      }
}

class ProducerConsumerDemo {
       public static void main(String[] args){
            Resource r = new Resource();
            Producer pro = new Producer(r);
            Consumer con = new Consumer(r);

            Thread t0 = new Thread(pro);
            Thread t1 = new Thread(pro);
            Thread t2 = new Thread(con);
            Thread t3 = new Thread(con);
            t0.start();
            t1.start();
            t2.start();
            t3.start();
      }
}



Lock接口

同步代码块就是对于锁的操作是隐式的。
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。

Lock接口的出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。

lock():获取锁。

unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。


Condition接口的出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。

Condition接口中的await方法对应于Object中的wait方法。

Condition接口中的signal方法对应于Object中的notify方法。

Condition接口中的signalAll方法对应于Object中的notifyAll方法。


使用一个Lock、一个Condition修改上面的多生产者-多消费者问题。

import java.util.concurrent.locks.*;
class Resource{
       private String name ;
       private int count = 1;
       private boolean flag = false;

      //创建一个锁对象
      Lock lock = new ReentrantLock();

       //通过已有的锁获取该锁上的监视器对象      
      Condition con = lock .newCondition();

       public void set(String name){
             lock.lock();
             try{
                   while(flag )
                         try{
                              con.await();
                        } catch(InterruptedException e){
                              e.printStackTrace();
                        }
                   this.name = name + count;
                   count++;
                   System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
                   flag = true ;
                   con.signalAll();
            }finally{
                   lock.unlock();
            }
      }

       public void out(){
            lock.lock();
             try{
                   while(!flag )
                         try{
                              con.await();
                        } catch(InterruptedException e){
                              e.printStackTrace();
                        }
                   flag = false ;
                   con.signalAll();
                   System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
            }finally{
                   lock.unlock();
            }
      }
}

class Producer implements Runnable{
       private Resource r ;
       Producer(Resource r){
             this.r = r;
       }
       public void run(){
             while(true ){
                   r.set( "烤鸭");
            }
      }
}

class Consumer implements Runnable{
       private Resource r ;
       Consumer(Resource r){
             this.r = r;
       }
       public void run(){
             while(true ){
                   r.out();
            }
      }
}

class ProducerConsumerDemo {
       public static void main(String[] args){
            Resource r = new Resource();
            Producer pro = new Producer(r);
            Consumer con = new Consumer(r);

            Thread t0 = new Thread(pro);
            Thread t1 = new Thread(pro);
            Thread t2 = new Thread(con);
            Thread t3 = new Thread(con);
            t0.start();
            t1.start();
            t2.start();
            t3.start();
      }
}



线程停止

当一个线程运行后,怎样才能让其停止呢?

通过 stop 方法就可以停止线程。但是这个方式过时了。

线程停止的原理就是让线程运行的代码结束,也就是结束 run 方法。

怎么结束 run 方法?

一般 run 方法里肯定定义循环。所以只要结束循环即可。

  • 定义循环的结束标记。
  • 如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过 Thread 类中的 interrupt 方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。


线程的其他方法

setDaemon方法

将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。

该方法必须在启动线程前调用。


join方法

等待该线程终止


setPriority方法

更改线程的优先级


toString方法

返回该线程的字符串表示形式,包括线程名称、优先级和线程组。


yield方法

暂停当前正在执行的线程对象,并执行其他线程。



补充

Java中的多线程你只要看这一篇就够了

Java多线程学习(吐血超详细总结)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值