Java多线程学习笔记

1.1线程和进程的区别

    进程是正在运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

  • 独立性:进程是系统中独立存在的实体,他可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序这是一个静态的指令集合,二进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念都是程序中不具备的。
  • 并发性:多个进程个在单个处理器上并发进行,多个进程之间不会互相影响

 

  并发性(concurrency)和并行性(parallel)是两个不同的概念。并行指在同一时刻,有多条指令在处理器上同时进行;并发指在同一时刻只有一条指令执行,但多个指令快速轮换执行,使得宏观上上具有多个进程同时执行的效果。

线程的特点:

  • 进程之间不可以共享内存,但是线程之间共享内存很容易。
  • 系统创建进程时需要为该进程重现分配系统资源,但创建线程则代价小得多,因此使用多线程来实现任务并发比多进程的效率高。
  • java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。

    java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收

2线程的创建和启动

一:继承Thread类

/*Created by Anshay on 2018年2月26日
*Email: anshaym@163.com
*Explaination:线程类的测试,继承Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量
*/
public class FirstThread extends Thread {
     int i;
     public static void main(String[] args) {
          for (int i = 0; i < 100; i++) {
               System.out.println(Thread.currentThread().getName()  + " " + i);
              if (i == 20) {
                   //new一个对象就是开启一条新线程
                   //线程可以使用setName(String name)为线程设置名字
                   new FirstThread().start();
                   new FirstThread().start();
                   FirstThread second = new  FirstThread();
                   second.setName("secondThread");
                   second.start();
                   //以上新建的三条线程加上main主线程即为一共有四条线程,主线程是由main()方法确定的
              }
          }
     }
     /* 重写run方法,run()方法就是线程的执行体 */
     public void run() {
          for (; i < 100; i++) {
              // 当线程继承Thread类时,直接使用this即可获取当前线程
              // Thread对象的getName()方法获取当前线程的名字
              // 因此,可以直接调用getName()方法返回当前线程的名字
              System.out.println(getName() + " " + i);
          }
     }
}

 

二:通过实现Runnable接口

/*Created by Anshay on 2018年2月26日
*Email: anshaym@163.com
*Explaination:通过实现Runnable接口来实现多线程,
*但是只能通过Thread.currentThread()方法获取到当前线程
*/

public class SecondThread implements Runnable {
     private int i;
     public static void main(String[] args) {
          for (int i = 0; i < 100; i++) {
               System.out.println(Thread.currentThread().getName()  + " " + i);
              if (i == 20) {
                   SecondThread st = new SecondThread();
                   // 通过new Thread(target,name)方法创建新线程
                   new Thread(st, "新线程1").start();
                   new Thread(st, "新线程2").start();
              }
          }
     }
     // run()方法仍然是线程执行体
     public void run() {
          for (; i < 100; i++) {
              // 当线程类实现Runnable接口时
              // 如果想获取当前线程,只能用Thread.currentThread()方法
              System.out.println(Thread.currentThread() +  " " + i);
          }
     }
}

 

 

区别:以上两种方法中,前者直接创建的Thread子类即可代表线程对象;后者创建的Runnable对象只能作为线程对象的target

3线程的生命周期

在线程的声明周期中,经过了New, Runnable, Running, Blockd, Dead 一共5种状态。

 

3.1新建和就绪状态

    当程序使用new关键字创建了一个线程后,该线程就处于新建状态。此时它和其他java对象一样,仅仅由java虚拟机为其分配内存,并初始化其成员变量。此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

    当线程对象调用了start()方法后,该线程处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

 

    启动线程调用的是start()方法,而不是run()方法,永远不要调用run()方法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接调用线程对象的run()方法,则run()方法就会立即被执行,而且在run()方法返回之前其他线程无法并发执行——也就是说,如果直接调用线程对象的run()方法,系统会把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

/*Created by Anshay on 2018年2月27日
*Email: anshaym@163.com
*Explaination:直接调用run()方法无法正常启动线程
*/

public class InvokeRun {
     private int i;
     public static void main(String[] args) {
          for (int i = 0; i < 100; i++) {
               System.out.println(Thread.currentThread().getName()  + " " + i);
              if (i == 20) {
                   /*
                    * 直接调用线程对象的run()方法,系统会把线程对象当成普通对象,把run()方法当成普通方法
                    * 所以下面两行代码不是新建两个线程,而是依次执行两次run()方法
                    */
                   new InvokeRun().run();
                   new InvokeRun().run();
              }
          }
     }
     public void run() {
          for (; i < 100; i++) {
              /*
               * 直接调用run方法时,Thread的this.getName()返回的是该对象的名字,而不是当前线程的名字
               * 使用Thread.currentThread().getName()总是获取当前线程的名字
               */
               System.out.println(Thread.currentThread().getName()  + " " + i);
          }
     }
}

3.2运行和阻塞状态

当发生如下情况是,线程会进入阻塞状态

  • 线程调用了sleep()方法主动放弃多占用的资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获取一个同步监视器,但是该同步监视器正在被其他线程所持有。
  • 线程在等待某个通知
  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用。

 

    当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态。即:被阻塞线程的阻塞解除后,必须重新等待线程调度器重新调度它。

    针对以上几种阻塞,发生如下几种情况可以解除阻塞

调用sleep()方法的线程经过了指定时间

  • 线程调用的阻塞式IO方法已经返回
  • 线程成功地获得了试图取得的同步监视器
  • 线程正在等待某个通知,其他线程发送了这个通知
  • 处于挂起的线程被调用了resume()方法

 

3.3线程死亡

    线程会以如下三种方式结束,结束后就处于死亡状态

  • run()方法后者call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或error
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

    当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动后,它就拥有和主线程一样地位,不受主线程的影响。

    可通过调用线程对象的isAlive()方法判断线程是否死亡。线程处于就绪,运行,阻塞状态时,返回true,处于新建、死亡状态时返回false。

public class StartDead extends Thread{
     private int i;
     //重写run方法
     public void run(){
          for(;i<100;i++){
              System.out.println(getName()+" "+ i);
          }
     }

     public static void main(String[] args) {
          StartDead sd = new StartDead();
          sd.setName("新线程");
          for(int i=0;i<100;i++){
              //打印当前线程的名字
               System.out.println(Thread.currentThread().getName()+"  "+i);
              if(i==20){
                   sd.start();
                   //判断启动后线程的isAlive()值,输出true
                   System.out.println(sd.isAlive());
              }
              //当线程处于新建、死亡两种状态时,isAlive()的值为false
              //当i>20时,线程肯定是启动过了的,如果sd.isAlive()为假时,为死亡状态
              sd.start();
          }
     }
}

    已经死亡的线程无法重新启动。会报出错误:

    java.lang.IllegalThreadStateException

程序只能对新建状态的线程调用star()方法,对新建状态的线程调用两次start()方法也是错误的,都会引发java.lang.IllegalThreadStateException异常。

4控制线程

    java的线程提供了一些便捷工具方法,通过使用这些方法可以很好的控制线程的执行。

    4.1 join线程

    Thred提供了一个线程等待另一个线程完成的方法——join()方法,在线程中,调用thread.join

public class JoinThread extends Thread{

     public JoinThread(String name){
          super(name);
     }

     //重写run方法
     public void run(){
          for(int i =0;i<100;i++){
              System.out.println(getName()+" "+i);
          }
     }

     public static void main(String[] args) throws Exception {
          // 启动子线程
          System.out.println("准备添加被join的线程");
          new JoinThread("新线程").start();
          for(int i =0;i<100;i++){
              if(i==20){
                   JoinThread jt = new JoinThread("被join的线程。");
                   jt.start();
                   //main线程中调用了jt线程的join()方法,main线程必须等jt线程执行结束才会向下进行
                   jt.join();
              }
               System.out.println(Thread.currentThread().getName()+" "+i);
          }
     }
}

 

    上面程序中一共有3个线程,主方法开始时就启动了名为“新线程”的子线程,该线程将会和main线程并发执行。当主线程的循环变量 i 等于20时,启动了名为“被join的线程”的线程,该线程不会和main线程并发执行,main线程需要等待该线程执行完毕后才能继续向下进行。此时只有两个线程进行。

4.2 后台线程

    有一类线程是在后台进行的,我们称为“后台线程(Daemon Thread)”或者“守护线程”。JVM的垃圾回收就是典型的后台线程。

    调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。以下程序将线程设为后台线程,当所有前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序也就没有运行的必要,所以虚拟机也就退出了。

public class DaemonThread extends Thread {

     public void run() {
          for (int i = 0; i < 1000; i++) {
              System.out.println(getName() + " " + i);
          }
     }

     public static void main(String[] args) {
          DaemonThread dt = new DaemonThread();
          // 将此线程设置为后台线程
          dt.setDaemon(true);
          dt.start();
          for (int i = 0; i < 10; i++) {
               System.out.println(Thread.currentThread().getName()  + " " + i);
          }
          // 程序执行到此处,前台线程(main线程)结束
          // 后台线程也随之结束
     }
}

后台还有一个isDaemon()方法判断是否为后台线程。

前台线程创建的子线程默认为前台线程,后台线程的子线程默认为后台线程。

4.3 线程睡眠:sleep

    static void sleep(long millis) : 让当前正在执行的线程暂停millis毫秒。

    static void sleep(long millis,int nanos) :  让当前正在执行的线程暂停millis毫秒加nanos微妙。(一般不用) 

    当当前线程调用sleep()方法进入阻塞状态后,在其睡眠的时间段里,线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep的线程也不会执行。

4.4 线程让步:yield

    yield()方法是一个和sleep()方法有点类似,也是Thread的一个静态方法,它也可以让当前的线程暂停,但不会阻塞线程,只是将线程转额就绪状态。该方法相当于跳过这次线程的执行,让系统的线程调度器重新调度一次。所以有可能出现:当某个线程调用了yield()方法暂停了以后,线程调度器又将其调度出来重新执行。

    实际上,当某个线程调用了yiel()方法暂停之后,只有优先级与当前相同,或者优先级更高的线程才会获得执行机会。

public class YieldTest extends Thread{

     //因为我们使用的是多CPU机器,所以不会出现暂停后只有同级或以上的线程执行的情况产生。
     public static void main(String[] args) {
          YieldTest yt1 = new YieldTest("高级");
          //将yt1线程设置为最高优先级
          yt1.setPriority(MAX_PRIORITY);
          yt1.start();
          YieldTest yt2 = new YieldTest("低级");
          yt2.setPriority(MIN_PRIORITY);
          yt2.start();
     }

     public YieldTest(String name){
          super(name);
     }

     public void run(){
          for (int i = 0; i < 100; i++) {
              System.out.println(getName()+ " "+i);
              if(i==20){
                   //当i等于20时,线程让步
                   Thread.yield();
              }
          }
     }
}

在多CPU并行的环境下,yield()方法下的功能优势后并不明显。如果是使用多核处理器的电脑,则不会出现当前线程让步后只有同级或以上线程被调度的情况发生。

4.5 改变线程优先级

Thread.currentThread.setPriority(int newPriority)

几个常量为:

    MAX_PRIORITY:   10

    MIN_PRIORITY:    1

    NORM_PRIORITY:    5

5线程同步

当多个线程对同一个对象进行访问时,就会容易出错,这里引入线程同步的概念。

5.1使用同步代码块的方式

synchronized(obj){
    //这里的代码就是同步代码块
}

"加锁—修改—释放锁"机制

public class DrawThread extends Thread {

     // 模拟用户账户
     private Account account;
     // 当前线程所希望取的钱数
     private double drawAmount;
     public DrawThread(String name, Account account,  double drawAmount) {
          super(name);
          this.account = account;
          this.drawAmount = drawAmount;
     }

     // 当有多个线程修改同一个共享数据时,将涉及数据安全问题
     public void run() {
          /*
           * 使用account作为同步监视器,任何线程进入下面的同步代码块之前,必须先获得对account的锁定
           * 其他线程无法获得锁,也就无法获得锁,也就无法修改它 这种做法符合:“加锁——修改——释放锁”的逻辑
           */
          synchronized (account) {
              // 吐出钞票
              if (account.getBanlance() >= drawAmount) {
                   System.out.println(getName() + "取钱成功!吐出钞票" + drawAmount);
                   try {
                        Thread.sleep(1);
                   } catch (InterruptedException ex) {
                        ex.printStackTrace();
                   }
                   //修改余额
                   account.setBanlance(account.getBanlance()-drawAmount);
                   System.out.println("\t余额为"+account.getBanlance());
              }else {
                   System.out.println(getName()+"取钱失败!");
              }
          }
          //同步代码块结束,该线程释放同步锁
     }
}

5.2 同步方法

将上述的同步代码块快更换成有synchronized修饰的同步方法

  • 该类对象可以被多个线程安全访问
  • 每个线程调用该对象的任意方法之后,该对象依然保持正确结果
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

注意:synchronized关键字可以修饰代码块,可以修饰方法,但不可修饰构造器和成员变量。

5.4 释放同步监视器的锁定

任何线程进入同步代码、同步方法之前,必须现货区对同步监视器的锁定。

释放同步监视器的情况:

  1. 遇到break,return终止了该代码块或者该方法的执行
  2. 遇到未处理的Error或者Exception,导致异常结束
  3. 程序执行了wait()方法

不释放同步监视器的情况

  1. 程序调用Thread.sleep()或者Thread.yield()方法暂停
  2. 代码块时,其他线程调用了该线程的suspend()方法将其挂起。(程序应尽量避免使用suspend()和resume()方法)

5.5 同步锁

通过显示定义同步锁对象来实现同步,同步锁对象由Lock对象充当。

class X {
          // 定义锁对象
          private final ReentrantLock lock = new  ReentrantLock();
          // 定义需要保证线程安全的方法
          public void m() {
              lock.lock();
              try {
              } finally {// 使用finally块来保证解锁
                   // 解锁
                   lock.unlock();
              }
          }
     }

 

    ReentrantLock具有可嵌套性,一个献策可以对已经被加锁的ReentrantLock锁在此加锁,锁对象会维持衣蛾极速器来追踪lock()方法的嵌套调用。线程每次调用lock()方法后必须使用unlock()方法解锁。

    一段被锁保护的代码可以调用另一个被相同锁保护的方法

5.6 死锁

死锁的四个必要条件:

  1. 互斥条件:一个资源只能被一线程
  2. 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,其他线程不能强行剥夺。
  4. 循环等待条件:两个及以上的线程形成一种头尾相连的循环资源等待关系。

6.线程通信

   可借助Object的wait(),notify()和notifyAll()三个方法。这三个方法必须由同步监视器对象调用。

  • synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可用上诉三种方法
  • synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象来调用这三个方法

关于这三个方法:

  • wait():导致当前线程等待,直到其他线程调用notify()或者notifyAll()方法。调用wait()方法会释放对该同步监视器的锁定。
  • noty():唤醒此同步监视器上等待的某个线程。选择是任意的。
  • notiyAll():唤醒此同步监视器上所有等待的线程。

6.2 使用condition控制线程通信

    当程序使用Lock对象来保证同步时,系统中不存在隐式的同步监视器,不可使用wait()、notify()、notifyAll()来进行线程通信了。

Condition提供三种方法:

  • await():类似于wait();调用
  • signal():唤醒此lock对象上等待的某个线程。只有当前线程放弃对该lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
  • signalAll():唤醒此Lock对象对象上等待的所有线程。
class X {
          // 定义锁对象
          private final ReentrantLock lock = new  ReentrantLock();
          // 获得指定Lock对象对应的Condition
          private final Condition cond = lock.newCondition();
          // 定义需要保证线程安全的方法
          public void m() {
              lock.lock();
              try {
                if(!flag){
                    cond.await();
                }else{
                //执行的逻辑代码
                    cond.signalAll();
                }
              } finally {// 使用finally块来保证解锁
                   // 解锁
                   lock.unlock();
              }
          }
     }

 

6.3 使用阻塞队列(BlockingQueue)控制线程通信

    BlockingQueue是java8中提供的一个Queue的子接口。

    特征:当生产线程试图向BlockingQueue中放入元素时,如果队列已满,则线程阻塞;挡消费者线程试图从BlockingQueue取出元素时,如果队列为空,则线程阻塞。

  • put(E e):尝试把e元素放入BlockingQueue中,如果队列已满,则阻塞当前线程;
  • take():尝试从BlockingQueue头部取出元素,如果队列元素已为空,则阻塞当前线程。

使用父类接口Queue中的方法:

在队列尾部插入元素:方法及队列已满时 

  • add(E e)—抛出异常,
  • offer()—返回false,
  • put(E e)—阻塞队列。

在队列头部删除并返回删除的元素:方法以及队列为空时

  • remove()—抛出异常
  • poll()—返回false
  • take()—阻塞队列

在队列头部取出但不删除元素:方法及队列为空时

  • element()—抛出异常
  • peek()—返回false

5个实现类:

ArrayBlockingQueue():基于数组实现的阻塞队列

LinkedBlockingQueue:基于链表实现的阻塞队列

PriorityBlockingQueue:非标准阻塞队列,与PriorityQueue类似,用remove(), poll(), take()等方法取出元素时取出的是队列中最小的元素而非存在时间最长的元素。

SynchronousQueue:同步队列

DelayQueue:特殊的阻塞队列,底层基于PriorityBlockingQueue实现,要求集合元素都实现Delay接口。根据集合元素的getDelay()方法的返回值进行排序。

下列程序不会打印“3”

public static void main(String[] args) throws  InterruptedException {
          //定义一个长度为2的阻塞队列
          BlockingQueue<String> bq = new  ArrayBlockingQueue<>(2);
          bq.put("java1");//与add(),offer()相同
          System.out.println("1");
          bq.put("java2");
          System.out.println("2");
          bq.put("java3");//阻塞队列
          System.out.println("3");
     }

7 线程组和未处理的异常

     java使用ThreadGroup来表示线程组,java允许程序直接对线程批量进行操作,载体即为线程组。子线程默认归入父线程的线程组。

    Thread提供几个默认的构造器设置新线程属于哪个线程组。

  • Thread(ThreadGroup group, Runable target):以target的run()方法作为线程执行体,线程归入group。
  • Thread (ThreadGroup group, Runable target ,String name):同上,且name为此线程的名字。
  • Thread (Thread group, String name):创建名为name的新线程,属于group线程组

    因为中途不可改变线程所属的线程组,所以Thread类未提供setThreadGroup()的方法,getThreadGroup()返回的是ThreadGroup对象

    Thread类的构造器:

  • ThreadGroup(String name):指定线程组的名字来新建线程组
  • ThreadGroup(ThreadGroup  parent, String name):指定线程组的父类线程组和名字来新建线程。

Thread类提供了如下几个方法来操作线程

  • int activeCount():返回此线程组中活动线程的数目
  • interrupt():中断此线程组中的所有线程
  • isDaemon():判断该线程组是否为后台线程组(后台线程组具有一个特征,当后台线程组的最后一个线程执行结束或者最后一个线程被销毁,后台线程组将自动销毁)
  • setMaxPriority(int pri):设置线程的最高优先级
public class ThreadGroupTest {

     public static void main(String[] args) {
          // 获取主线程所在的线程组,这是多有线程的默认线程组
          ThreadGroup mainGroup =  Thread.currentThread().getThreadGroup();
          System.out.println("主线程是否为后台线程" +  mainGroup.isDaemon());
          new MyThread("主线程组的线程").start();
          ThreadGroup tg = new ThreadGroup("新线程组");
          tg.setDaemon(true);
          System.out.println("线程组tg是否为后台线程:" +  tg.isDaemon());
          MyThread tt = new MyThread(tg, "tg线程组的线程甲");
          tt.start();
          new MyThread(tg, "tg线程组的线程乙").start();
     }
}

class MyThread extends Thread {
     // 提供指定线程名的构造器
     public MyThread(String name) {
          super(name);
     }
     // 提供指定线程名、线程的构造器
     public MyThread(ThreadGroup group, String name) {
          super(group, name);
     }
     public void run() {
          for (int i = 0; i < 100; i++) {
              System.out.println(getName() + " " + i);
          }
     }
}

 

ThreadGroup内部定义一个方法:

void uncaughtException(Thread t, Throwable e),此方法可以处理线程组中任意线程抛出的异常

Thread类提供以下两个方法来设置异常处理

  • static setDefaultUncaughtExceptionHandler(Thrad.UncaughtException eh):为该线程类的所有线程实例设置默认的异常处理器。
  • setUncaughtExceptionHandler(Thrad.UncaughtException eh):为指定的线程实例设置异常处理器。

    ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组会作为默认的异常处理器。当一个线程抛出异常时,JVM会首先查找该异常对应的异常处理器,如果找到则调异常处理器用处理该异常;否则,JVM将会调用该线程所属的线程组对象的uncaughtException ()方法来处理该异常。

    默认流程如下:

  1. 如果该线程组有父类线程组,则调用父类的方法处理。
  2. 如果该线程实例所属的线程组有默认的异常处理器,那么调用处理。
  3. 如果该异常对象时ThreadDeath的对象,则不做任何处理。
public class ExHandler {
     public static void main(String[] args) {
          Thread.currentThread().setUncaughtExceptionHandler(new  MyExHandler());
          // 在此处产生异常
          // Thread[main,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero
          int a = 5 / 0;
          System.out.println("程序正常结束");

     }
}

// 自定义的异常处理器
class MyExHandler implements  Thread.UncaughtExceptionHandler {
     // 实现uncaughtException方法,该方法将处理线程的未处理异常
     public void uncaughtException(Thread t, Throwable e)  {
          System.out.println(t + "线程出现了异常:" + e);
     }
}

当使用了catch捕获异常后异常不会向上传递,但异常处理器进行异常处理后仍会向上传递

8 线程池

    因为涉及与操作系统交互,系统启动一个新线程的成本较高。所以使用线程可以很好的提高性能。(尤其是当程序需要创建大量生存期很短的线程时)

    与数据库类似的是,线程池在系统启动时就会创建大量空闲的线程,程序将一个Runable对象或Callable对象传给线程池,线程池就启动一个线程来执行他们的run()方法或者call()方法,当run()方法或者call()方法执行结束之后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待下一个Runnable对象或者Callable对象传入。

调用线程池来执行线程任务的步骤:

  1. 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象表示一个线程池。
  2. 调用Runnable实现类或者Callable实现类的实例,作为线程执行任务
  3. 调用ExecutorService对象的submit()方法来提交Runnable或者Callbale实例
  4. 当不想提交任何任务时,调用ExecutorService的shutdown()方法来关闭线程池。

如下代码使用线程池来执行指定Runnable对象所代表的对象

public class ThreadPoolTest {
     public static void main(String[] args)throws  Exception {
          ExecutorService pool =  Executors.newFixedThreadPool(6);
          //使用Lambda方式创建Runnable对象
          Runnable target = ()->{
              for(int i = 0; i<100; i++){
                   System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
              }
          }
          pool.submit(target);
          pool.submit(target);
          pool.shutdown();
     }
}

8.2 ForkJoinPool

ForkJoinPool支持将一个任务拆分成多个小任务并进行计算,再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。

构造器:

  • ForkJoinPool(int parallelism): 创建一个包含parallelism个并行线程的ForkJoinPool。
  • ForkJoinPool():  以Runnable.availableProcessors()方法的返回值作为parallelism参数来创建ForkPool。

提供如下静态方法:

  • commonPool(): 返回一个通用池,通用池的运行状态不会受shutdown()或shutdownNow()方法的影响。如果程序调用System.exit(0);来终止虚拟机,通用池以及通用池中正在执行的任务都会被终止。
  • getCommomPoolParallelism():返回通用池的并行级别。

    创建ForkJoinPool实例后,就可以调用submit(ForkJoinTask task) 或invoke(ForkJoinTask task )方法来执行指定的任务。ForkJoinTask是一个抽象类,还有两个抽象子类:RecursiveAction和RecursiveTask。前者代表无返回值的任务,后者代表有返回值的任务。

public class ForkJoinPoolTest {
     public static void main(String[] args) throws  Exception {
          ForkJoinPool pool = new ForkJoinPool();
          //提交可分解的PrinkTask
          pool.submit(new PrintTask(0, 300));
          pool.awaitTermination(2, TimeUnit.SECONDS);
          pool.shutdown();
     }
}

class PrintTask extends RecursiveAction {
     private static final int THRESHOLD = 50;
     private int start;
     private int end;
     public PrintTask(int start, int end) {
          this.start = start;
          this.end = end;
     }
     @Override
     protected void compute() {
          // TODO Auto-generated method stub
          if ((end - start) < THRESHOLD) {
              for (int i = start; i < end; i++) {
                   System.out.println(Thread.currentThread().getName()  + "的i值为:"
                             + i);
              }
          } else {
              // 当任务大于额定值,将其拆分为两个小任务
              int middle = (start + end) / 2;
              PrintTask left = new PrintTask(start, middle);
              PrintTask right = new PrintTask(middle,  end);
              left.fork();
              right.fork();
          }
     }
}

要加起来的时候用  join()  方法。

 

9 线程相关类

    ThreadLocal类代表一个线程局部变,通过吧数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本。,从而避免并发访问的线程的安全问题。

    线程局部变量为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立的改变自己的副本,而不会和其他线程的副本冲突。

     ThreadLocal提供了三个public方法:

  • get():    返回此线程局部变量中单钱线程副本中的值。
  • remove():删除此线程局部变量中当前线程副本中的值。
  • set(value):设置此局部变量中当前线程副本中的值
public class ThreadLocalTest {
     public static void main(String[] args) {
          // 启动两个线程,两个线程共享同一个account
          AccountT account = new AccountT("初始名");
          new MyTest(account, "线程甲").start();
          new MyTest(account, "线程乙").start();
     }
}
class AccountT {
     // 定义一个ThreadLocal类型的变量,该变量是一个线程局部变量,每个线程都会保留该变量的一个副本
     private ThreadLocal<String> name = new  ThreadLocal<>();
     // 定义一个初始化name成员变量的构造器
     public AccountT(String str) {
          name.set(str);
          // 访问用于访问当前线程的name副本的值
          System.out.println("---" + this.name.get());
     }
     public String getName() {
          return name.get();
     }
     public void setName(String name) {
          this.name.set(name);
     }
}
class MyTest extends Thread {
     private AccountT account;
     public MyTest(AccountT account, String name) {
          super(name);
          this.account = account;
     }
     public void run() {
          // 当i==6时输出将账号名替换成当前线程名
          for (int i = 0; i < 100; i++) {
              System.out.println(account.getName() + "账户的i的值" + i);
              if (i == 6) {
                   account.setName(getName());
                   System.out.println(account.getName() +  "账户的i的值" + i);
              }
          }
     }
}

普通的同步机制是通过对象加锁的来实现多个线程对同一变量的安全访问。

该变量是多线程共享的。

    ThreadLocal从另一个角度来解决多线程的并发访问, ThreadLocal将需要的资源复制多份,每个线程拥有一份资源,每个线程都有自己的资源副本,从而也就没有必要对该变量变量进行同步了。

ThreadLocal并不能替代同步机制,两者面向的问题领域不同,同步机制是为了同步多个线程对相同机制的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

 

以上:学习java多线程的笔记

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值