当前位置: 移动技术网 > IT编程>开发语言>Java > 整理总结Java多线程程序编写的要点

整理总结Java多线程程序编写的要点

2019年07月22日  | 移动技术网IT编程  | 我要评论

线程状态图

201613084914746.jpg (700×480)

线程共包括以下5种状态。

1. 新建状态(new)         : 线程对象被创建后,就进入了新建状态。例如,thread thread = new thread()。

2. 就绪状态(runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被cpu调度执行。

3. 运行状态(running) : 线程获取cpu权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4. 阻塞状态(blocked)  : 阻塞状态是线程因为某种原因放弃cpu使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。

    (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

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

5. 死亡状态(dead)    : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

实现多线程的方式thread和runnable
thread:继承thread类,实现run方法,在main函数中调用start方法启动线程

runnable:接口,实现runnable接口,作为参数传递给thread的构造函数,在main中调用start方法

例子:

class task implements runnable {
 
  private int ticket = 10;
 
  @override
  public void run() {
    for (int i = 0; i < 20; i++) {
      if (this.ticket > 0) {
        system.out.println(thread.currentthread().getname()
            + " sold ticket " + this.ticket--);
      }
    }
  }
 
};
 
 
public class runnabletest {
  public static void main(string[]args){
    task mytask = new task();
    thread t1 = new thread(mytask);
    thread t2 = new thread(mytask);
    thread t3 = new thread(mytask);
    t1.start();
    t2.start();
    t3.start();
  }
 
}

//threadtest.java 源码
class mythread extends thread {
  private int ticket = 10;
 
  public void run() {
    for (int i = 0; i < 20; i++) {
      if (this.ticket > 0) {
        system.out.println(this.getname() + " 卖票:ticket"
            + this.ticket--);
      }
    }
  }
}
 
public class threadtest {
  public static void main(string[] args) {
    // 启动3个线程t1,t2,t3;每个线程各卖10张票!
    mythread t1 = new mythread();
    mythread t2 = new mythread();
    mythread t3 = new mythread();
    t1.start();
    t2.start();
    t3.start();
  }
};


thread与runnable的区别

thread 是类,而runnable是接口;thread本身是实现了runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此runnable具有更好的扩展性。此外,runnable还可以用于“资源的共享”。即,多个线程都是基于某一个runnable对象建立的,它们会共享runnable对象上的资源。通常,建议通过“runnable”实现多线程!

thread的run与start

start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。start()实际上是通过本地方法start0()启动线程的。而start0()会新运行一个线程,新线程会调用run()方法。

run()   : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!run()就是直接调用thread线程的runnable成员的run()方法,并不会新建一个线程。

// demo.java 的源码
class mythread extends thread{ 
  public mythread(string name) {
    super(name);
  }
 
  public void run(){
    system.out.println(thread.currentthread().getname()+" is running");
  } 
}; 
 
public class demo { 
  public static void main(string[] args) { 
    thread mythread=new mythread("mythread");
 
    system.out.println(thread.currentthread().getname()+" call mythread.run()");
    mythread.run();
 
    system.out.println(thread.currentthread().getname()+" call mythread.start()");
    mythread.start();
  } 
}

输出:

main call mythread.run()
main is running
main call mythread.start()
mythread is running


synchronized
在java中每个对象都有一个同步锁,当我们调用对象的synchronized方法就获取了对象锁,synchronized(obj)就获取了“obj这个对象”的同步锁.不同线程对同步锁的访问是互斥的.某时间点对象的同步锁只能被一个线程获取到.通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问.  例如,现在有两个线程a和线程b,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程a获取到“obj的同步锁”并在执行一些操作;而此时,线程b也企图获取“obj的同步锁” —— 线程b会获取失败,它必须等待,直到线程a释放了“该对象的同步锁”之后线程b才能获取到“obj的同步锁”从而才可以运行。

基本规则

第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。

第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

synchronized 方法

public synchronized void foo1() {
  system.out.println("synchronized methoed");
}
synchronized代码块

public void foo2() {  
    synchronized (this) {
    system.out.println("synchronized methoed");
    }
}

synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。

synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率

实例锁 和 全局锁
实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。

全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

pulbic class something {  
  public synchronized void issynca(){}  
  public synchronized void issyncb(){}  
  public static synchronized void csynca(){}  
  public static synchronized void csyncb(){}
}


(01) x.issynca()与x.issyncb() 不能被同时访问。因为issynca()和issyncb()都是访问同一个对象(对象x)的同步锁!

(02) x.issynca()与y.issynca()  可以同时被访问。因为访问的不是同一个对象的同步锁,x.issynca()访问的是x的同步锁,而y.issynca()访问的是y的同步锁。

(03) x.csynca()与y.csyncb()不能被同时访问。因为csynca()和csyncb()都是static类型,x.csynca()相当于something.issynca(),y.csyncb()相当于something.issyncb(),因此它们共用一个同步锁,不能被同时反问。

(04) x.issynca()与something.csynca() 可以被同时访问。因为issynca()是实例方法,x.issynca()使用的是对象x的锁;而csynca()是静态方法,something.csynca()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。


线程阻塞与唤醒wait,notify,notifyall
在object.java中,定义了wait(), notify()和notifyall()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyall()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyall()是唤醒所有的线程。

object类中关于等待/唤醒的api详细信息如下:

notify()       -- 唤醒在此对象监视器上等待的单个线程。

notifyall()   -- 唤醒在此对象监视器上等待的所有线程。

wait()         -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法”,当前线程被唤醒(进入“就绪状态”)。

wait(long timeout)      -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

wait(long timeout, int nanos)  -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

// waittest.java的源码
class threada extends thread{
 
  public threada(string name) {
    super(name);
  }
 
  public void run() {
    synchronized (this) {
      system.out.println(thread.currentthread().getname()+" call notify()");
      // 唤醒当前的wait线程
      notify();
    }
  }
}
 
public class waittest {
 
  public static void main(string[] args) {
 
    threada t1 = new threada("t1");
 
    synchronized(t1) {
      try {
        // 启动“线程t1”
        system.out.println(thread.currentthread().getname()+" start t1");
        t1.start();
 
        // 主线程等待t1通过notify()唤醒。
        system.out.println(thread.currentthread().getname()+" wait()");
        t1.wait();
 
        system.out.println(thread.currentthread().getname()+" continue");
      } catch (interruptedexception e) {
        e.printstacktrace();
      }
    }
  }
}

输出

main start t1
main wait()
t1 call notify()
main continue

(01) 注意,图中"主线程" 代表“主线程main”。"线程t1" 代表waittest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。

(02) “主线程”通过 new threada("t1") 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。

(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyall()将其唤醒。

(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。

(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。

t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!

package thread.test;
 
public class notifyalltest {
 
  private static object obj = new object();
  public static void main(string[] args) {
 
    threada t1 = new threada("t1");
    threada t2 = new threada("t2");
    threada t3 = new threada("t3");
    t1.start();
    t2.start();
    t3.start();
 
    try {
      system.out.println(thread.currentthread().getname()+" sleep(3000)");
      thread.sleep(3000);
    } catch (interruptedexception e) {
      e.printstacktrace();
    }
 
    synchronized(obj) {
      system.out.println(thread.currentthread().getname()+" notifyall()");
      obj.notifyall();//在此唤醒t1.t2.t3
    }
  }
 
  static class threada extends thread{
 
    public threada(string name){
      super(name);
    }
 
    public void run() {
      synchronized (obj) {
        try {
          // 打印输出结果
          system.out.println(thread.currentthread().getname() + " wait");
 
          //释放obj对象锁
          obj.wait();
 
          // 打印输出结果
          system.out.println(thread.currentthread().getname() + " continue");
        } catch (interruptedexception e) {
          e.printstacktrace();
        }
      }
    }
  }
}

输出:

t1 wait
main sleep(3000)
t3 wait
t2 wait
main notifyall()
t2 continue
t3 continue
t1 continue


(01) 主线程中新建并且启动了3个线程"t1", "t2"和"t3"。

(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设"t1", "t2"和"t3"这3个线程都运行了。以"t1"为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityall()来唤醒它;相同的道理,"t2"和"t3"也会等待其它线程通过nofity()或nofityall()来唤醒它们。

(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyall() 唤醒obj上的等待线程,即唤醒"t1", "t2"和"t3"这3个线程。 紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,"t1", "t2"和"t3"就可以获取“obj锁”而继续运行了!

notify,notifyall与锁的关系

object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。

wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!

ok,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyall()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。

负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyall()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。

总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在object类,而不是thread类中的原因。


线程让步yield
线程让步,使线程从执行状态变为就绪状态,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行。

yield 与wait

(01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。

(02) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

(03) wait是object的方法,yield是thread的方法

线程休眠 sleep
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

sleep与wait的区别

wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态。(这个其实区别不大)

wait()会释放对象的同步锁,而sleep()则不会释放锁

 wait是object的方法,sleep是thread的方法

join
让主线程等待,子线程运行完毕,主线程才能继续运行

interrupt
用来终止处于阻塞状态的线程

@override
public void run() {
  try {
    while (true) {
      // 执行任务...
    }
  } catch (interruptedexception ie) { 
    // 由于产生interruptedexception异常,退出while(true)循环,线程终止!
  }
}

在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生interruptedexception中断。中断的捕获在while(true)之外,这样就退出了while(true)循环

终止处于运行状态的线程

@override
public void run() {  
  while (!isinterrupted()) {    
  // 执行任务...  
  }
}

通用的终止线程的方式

@override
public void run() {
  try {
    // 1. isinterrupted()保证,只要中断标记为true就终止线程。
    while (!isinterrupted()) {
      // 执行任务...
    }
  } catch (interruptedexception ie) { 
    // 2. interruptedexception异常保证,当interruptedexception异常产生时,线程被终止。
  }
}


线程优先级
java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。java 中有两种线程:用户线程和守护线程。可以通过isdaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:java虚拟机在“用户线程”都结束后会后退出。

每个线程都有一个优先级。“高优先级线程”会优先于“低优先级线程”执行。每个线程都可以被标记为一个守护进程或非守护进程。在一些运行的主线程中创建新的子线程时,子线程的优先级被设置为等于“创建它的主线程的优先级”,当且仅当“创建它的主线程是守护线程”时“子线程才会是守护线程”。

当java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。jvm会一直运行直到下面的任意一个条件发生,jvm就会终止运行:

(01) 调用了exit()方法,并且exit()有权限被正常执行。

(02) 所有的“非守护线程”都死了(即jvm中仅仅只有“守护线程”)。

守护进程

(01) 主线程main是用户线程,它创建的子线程t1也是用户线程。

(02) t2是守护线程。在“主线程main”和“子线程t1”(它们都是用户线程)执行完毕,只剩t2这个守护线程的时候,jvm自动退出。

生产者消费者问题
(01) 生产者仅仅在仓储未满时候生产,仓满则停止生产。

(02) 消费者仅仅在仓储有产品时候才能消费,仓空则等待。

(03) 当消费者发现仓储没产品可消费时候会通知生产者生产。

(04) 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

线程之间的通信

方式:共享内存和消息传递

共享内存:线程a和线程b共享内存,线程a更新共享变量的值,刷新到主内存中,线程b去主内存中读取线程a更新后的变量。整个通信过程必须通过主内存。同步是显式进行的。

201613085415380.gif (570×496)

如果一个变量是volatile类型,则对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

volatile变量自身具有下列特性:

[可见性]:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

[原子性]:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写:当写一个volatile变量时,jmm会把该线程对应的本地内存中的共享变量刷新到主内存。

volatile读:当读一个volatile变量时,jmm会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

消息传递:消息的发送在消息的接受之前,同步隐式进行。

threadlocal

threadlocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过threadlocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.threadlocal使得各线程能够保持各自独立的一个对象,并不是通过threadlocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过threadlocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行threadlocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,threadlocal实例是作为map的key来使用的。 如果threadlocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的threadlocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

threadlocal的一个实现

import java.util.collections; 
import java.util.hashmap; 
import java.util.map; 
 
/** 
* 使用了threadlocal的类 
* 
* @author leizhimin 2010-1-5 10:35:27 
*/ 
public class mythreadlocal { 
 
    //定义了一个threadlocal变量,用来保存int或integer数据 
    private com.lavasoft.test2.threadlocal<integer> tl = new com.lavasoft.test2.threadlocal<integer>() { 
        @override 
        protected integer initialvalue() { 
            return 0; 
        } 
    }; 
 
    public integer getnextnum() { 
        //将tl的值获取后加1,并更新设置t1的值 
        tl.set(tl.get() + 1); 
        return tl.get(); 
    } 
} 
 
class threadlocal<t> { 
    private map<thread, t> map = collections.synchronizedmap(new hashmap<thread, t>()); 
 
    public threadlocal() { 
    } 
 
    protected t initialvalue() { 
        return null; 
    } 
 
    public t get() { 
        thread t = thread.currentthread(); 
        t obj = map.get(t); 
        if (obj == null && !map.containskey(t)) { 
            obj = initialvalue(); 
            map.put(t, obj); 
        } 
        return obj; 
    } 
 
    public void set(t value) { 
        map.put(thread.currentthread(), value); 
    } 
 
    public void remove() { 
        map.remove(thread.currentthread()); 
    } 
}

事实上threadlocal是这样做的:

  public t get() {
    thread t = thread.currentthread();
    threadlocalmap map = getmap(t);
    if (map != null) {
      threadlocalmap.entry e = map.getentry(this);
      if (e != null)
        return (t)e.value;
    }
    return setinitialvalue();
  }

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网