JAVA 多线程编程


Java 多线程编程
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
如图:
在这里插入图片描述

- 新建状态:

使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

- 就绪状态:

当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

- 运行状态:

如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

- 阻塞状态:

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

等待阻塞:

运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

同步阻塞:

线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

其他阻塞:

通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态:

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
打个比方就是 你在食堂打饭 要排队吧 不然 岂不是乱套 你要是先排队,有人插队 肯定会有很多人有意见

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

优先级取值范围

Java 线程优先级使用 1 ~ 10 的整数表示:

  • 最低优先级 1:Thread.MIN_PRIORITY
  • 最高优先级 10:Thread.MAX_PRIORITY
  • 普通优先级 5:Thread.NORM_PRIORITY

获取线程优先级

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getPriority());//计算优先级
}

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
在这里插入图片描述

设置优先级

Java 使用 setPriority 方法设置线程优先级,方法签名

public final void setPriority(int newPriority)

示例:
在这里插入图片描述
注意:
newPriority 设置范围在 1-10,否则抛出 java.lang.IllegalArgumentException 异常
不多说 上图演示:

在这里插入图片描述

创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程;

通过实现 Runnable 接口来创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。

为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

public void run()

你可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,下面的这个是我们经常使用的:

Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

void start();

样例:

class RunnableDemo implements Runnable {
    private Thread t;
    private String threadName;

    RunnableDemo( String name) {
        threadName = name;
        System.out.println("Creating " +  threadName );
    }

    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 4; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", " + i);
                // 让线程睡眠一会
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }

    public void start () {
        System.out.println("Starting " +  threadName );
        if (t == null) {
            t = new Thread (this, threadName);
            t.start ();
        }
    }
}

public class test {
    public static void main(String[] args) {
        RunnableDemo R1 = new RunnableDemo( "Thread-1");
        R1.start();
        RunnableDemo R2 = new RunnableDemo( "Thread-2");
        R2.start();
    }
}


输出结果:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-2, 2
Thread: Thread-1, 2
Thread: Thread-2, 1
Thread: Thread-1, 1
Thread Thread-2 exiting.
Thread Thread-1 exiting.

通过继承Thread来创建线程

创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

class ThreadDemo extends Thread {
   private Thread t;
   private String threadName;
   
   ThreadDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 让线程睡眠一会
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}
 
public class Test{
 
   public static void main(String args[]) {
      ThreadDemo T1 = new ThreadDemo( "Thread-1");
      T1.start();
      
      ThreadDemo T2 = new ThreadDemo( "Thread-2");
      T2.start();
   }   
}

结果:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Thread 方法
下表列出了Thread类的一些重要方法:

序号 方法描述

  • 1 public void start()

使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

  • 2 public void run()

如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run
方法;否则,该方法不执行任何操作并返回。

  • 3 public final void setName(String name)

改变线程名称,使之与参数 name 相同。

  • 4 public final void setPriority(int priority)

更改线程的优先级。

  • 5 public final void setDaemon(boolean on)

将该线程标记为守护线程或用户线程。

  • 6 public final void join(long millisec)

等待该线程终止的时间最长为 millis 毫秒。

  • 7 public void interrupt()

中断线程。

  • 8 public final boolean isAlive()

测试线程是否处于活动状态。
上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

序号 方法描述

  • 1 public static void yield()

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

  • 2 public static void sleep(long millisec)

在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

  • 3 public static boolean holdsLock(Object x)

当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。

  • 4 public static Thread currentThread()

返回对当前正在执行的线程对象的引用。

  • 5 public static void dumpStack()

将当前线程的堆栈跟踪打印至标准错误流。

通过 Callable 和 Future 创建线程

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 50;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<50;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

来跑一下:
看结果

main 的循环变量i的值0
main 的循环变量i的值1
main 的循环变量i的值2
main 的循环变量i的值3
main 的循环变量i的值4
main 的循环变量i的值5
main 的循环变量i的值6
main 的循环变量i的值7
main 的循环变量i的值8
main 的循环变量i的值9
main 的循环变量i的值10
main 的循环变量i的值11
main 的循环变量i的值12
main 的循环变量i的值13
main 的循环变量i的值14
main 的循环变量i的值15
main 的循环变量i的值16
main 的循环变量i的值17
main 的循环变量i的值18
main 的循环变量i的值19
main 的循环变量i的值20
main 的循环变量i的值21
main 的循环变量i的值22
main 的循环变量i的值23
main 的循环变量i的值24
main 的循环变量i的值25
main 的循环变量i的值26
main 的循环变量i的值27
main 的循环变量i的值28
main 的循环变量i的值29
main 的循环变量i的值30
有返回值的线程 0
有返回值的线程 1
main 的循环变量i的值31
main 的循环变量i的值32
main 的循环变量i的值33
main 的循环变量i的值34
有返回值的线程 2
main 的循环变量i的值35
main 的循环变量i的值36
main 的循环变量i的值37
main 的循环变量i的值38
main 的循环变量i的值39
main 的循环变量i的值40
main 的循环变量i的值41
main 的循环变量i的值42
main 的循环变量i的值43
有返回值的线程 3
main 的循环变量i的值44
有返回值的线程 4
main 的循环变量i的值45
有返回值的线程 5
main 的循环变量i的值46
有返回值的线程 6
main 的循环变量i的值47
有返回值的线程 7
main 的循环变量i的值48
有返回值的线程 8
main 的循环变量i的值49
有返回值的线程 9
有返回值的线程 10
有返回值的线程 11
有返回值的线程 12
有返回值的线程 13
有返回值的线程 14
有返回值的线程 15
有返回值的线程 16
有返回值的线程 17
有返回值的线程 18
有返回值的线程 19
有返回值的线程 20
有返回值的线程 21
有返回值的线程 22
有返回值的线程 23
有返回值的线程 24
有返回值的线程 25
有返回值的线程 26
有返回值的线程 27
有返回值的线程 28
有返回值的线程 29
有返回值的线程 30
有返回值的线程 31
有返回值的线程 32
有返回值的线程 33
有返回值的线程 34
有返回值的线程 35
有返回值的线程 36
有返回值的线程 37
有返回值的线程 38
有返回值的线程 39
有返回值的线程 40
有返回值的线程 41
有返回值的线程 42
有返回值的线程 43
有返回值的线程 44
有返回值的线程 45
有返回值的线程 46
有返回值的线程 47
有返回值的线程 48
有返回值的线程 49
子线程的返回值:50

这个的话 其实改一下循环值 数据就会有不同,个人感觉 还是看cpu了,电脑性能不同 这个数据应该也不一样。

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

采用实现Runnable、Callable接口的方式创见多线程时
优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时
优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。

线程的几个主要概念

在多线程编程时,你需要了解以下几个概念:
下面 我们来浅谈一下:(因为我也没有特别深入的了解 希望暑假的时候 我在完善一下)

线程同步

因为我写的挺烂的 兄弟们可以看一下这个大佬的!

线程同步概念

当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中CPU不要切换到其它线程工作,这时就需要同步。如果两段代码是同步的,那么同一时间只会执行其中一个,这个结束之前另外一段代码就不会被执行。

这个就像上面的那个 T1.start();和T2.start();的那个 有印象的老哥应该还记得那个输出结果

线程同步作用

线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。

当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。

线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

线程同步方法

  • wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException异常。
  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的
    唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
  • notityAll ():唤醒所有处于等待状态的线程,注意并不是给所有唤醒线程一个对象的锁, 而是让它们竞争。

线程同步实现方法

同步方法
即有synchronized关键字修饰的方法。 
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如:

 public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

同步代码块
即有synchronized关键字修饰的语句块。 
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
synchronized(object){ }
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 
使用特殊域变量(volatile)实现线程同步

volatile关键字为域变量的访问提供了一种免锁机制,
使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
因此每次使用该域就要重新计算,而不是使用寄存器中的值
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

//只给出要修改的代码,其余代码与上同
class Bank {
 
    //需要同步的变量加上volatile
    private volatile int account = 100;
 
    public int getAccount() {
        return account;
    }
 
    //这里不再需要synchronized 
    public void save(int money) {
        account += money;
    }

线程间通信

为什么需要线程通信

  1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
  2. 当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
  3. 所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

什么是线程通信?

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。

就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。

于是我们引出了等待唤醒机制:(wait()、notify())

就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

其实说白了 就是 生产者和消费者模式

线程死锁

死锁概述

  • 线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。

死锁产生的条件

  • 互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。
  • 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
  • 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁
  • 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。

线程控制:挂起、停止和恢复

这里只做一下简单的赘述

挂起和恢复

wait()与notify()方法

wait()方法同样可以使线程进行挂起操作,调用了wait()方法的线程进入了“非可执行”状态,使用wait()方法有两种方式,例如:

thread.wait(1000);
或:
thread.wait();
thread.notify();

其中第一种方式给定线程挂起时间,基本上与sleep()方法用法相同。第二种方式是wait()与notify()方法配合使用,这种方式让wait()方法无限等下去,直到线程接收到notify()或notifyAll()消息为止。

wait()、notify()、notifyAll()不同于其他线程方法,这3个方法是java.lang.Object类的一部分,所以在定义自己类时会继承下来。wait()、notify()、notifyAll()都被声明为final类,所以无法重新定义。

join()方法:

能够使当前执行的线程停下来等待,直至join()方法所调用的那个线程结束,再恢复执行。例如如果有一个线程A正在运行,用户希望插入一个线程B,并且要求线程B执行完毕,然后再继续线程A,此时可以使用join()方法来完成这个需求。

thread.suspend()和thread.resume()

使用 thread.suspend()方法暂停线程,使用 thread.resume()恢复暂停的线程 的特点。(这个现在好像已经淘汰了 因为会产生死锁)

sleep()方法

是一个使线程暂时停止一段执行时间的方法,该时间由给定的毫秒数决定。

class ThreadA extends Thread
{
	public void run(){
		System.out.println("ThreadA is running");
	}
}
 
public class TestNew {
	public static void main(String[] args)throws InterruptedException {
		// TODO Auto-generated method stub
		ThreadA ta = new ThreadA();
		ta.start();
		ta.sleep(5000); //等待5000ms
		System.out.println("TestNew is running");
	}
}

终止

使用退出标志终止线程

当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。
如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

使用stop方法终止线程

使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:

thread.stop(); 
使用interrupt方法终止线程

使用interrupt方法来终端线程可分为两种情况:

(1)线程处于阻塞状态,如使用了sleep方法。

(2)使用while(!isInterrupted()){……}来判断线程是否被中断。

在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。
下面的代码演示了在第一种情况下使用interrupt方法。


public class ThreadInterrupt extends Thread  
{  
    public void run()  
    {  
        try  
        {  
            sleep(50000);  // 延迟50秒  
        }  
        catch (InterruptedException e)  
        {  
            System.out.println(e.getMessage());  
        }  
    }  
    public static void main(String[] args) throws Exception  
    {  
        Thread thread = new ThreadInterrupt();  
        thread.start();  
        System.out.println("在50秒之内按任意键中断线程!");  
        System.in.read();  
        thread.interrupt();  
        thread.join();  
        System.out.println("线程已经退出!");  
    }  
}  

好了 今天就到这里了 下次见!
在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值