多线程的基本实现

     进程指运行中的程序,每个进程都会分配一个内存空间,一个进程中存在多个线程,启动一个JAVA虚拟机,就是打开个一个进程,一个进程有多个线程,当多个线程同时进行,就叫并发。

   J ava创建线程的两种方式为: 继承Thread类 和实现Runnable接口

   Thread类

   1、通过覆盖run方法实现线程要执行的程序代码

   2、Start()开始执行多线程

package com.bin.duoxiancheng;
public class d1 extends Thread{
    public void run(){
       for(int i=0 ; i<50; i++){
           System.out.println(i);
           System.out.println(currentThread().getName());
           try {
              sleep(100);
           } catch (InterruptedException e) {
              // TODO Auto-generatedcatch block
              e.printStackTrace();
           }
       }
    }
    public static void main(String[] args){
       new d1().start();
       new d1().start();
    }
}


多个线程共享一个实例的时候,代码代码如下:

package com.bin.duoxiancheng;
public class d1 extends Thread{
    int i=0;
    public void run(){
       for(i=0 ; i<50; i++){
           System.out.println(i);
           System.out.println(currentThread().getName());
           try {
              sleep(100);
           } catch (InterruptedException e) {
              // TODO Auto-generatedcatch block
              e.printStackTrace();
           }
       }
    }
    public static void main(String[] args){
       new d1().start();
       new d1().start();
    }
}


结果如下所示:

0

Thread-1

0

Thread-0

1

Thread-1

1

实际2个线程在操纵不同的变量a,在执行run方法时候,线程把a都当做自己的变量在执行。

Runnable接口实现多线程

当一个继承自Thread时,就不能再继承其他类,使用Runnable接口解决了此问题,在新建一个Thread类中,在构造方法中初始化

Thread(Runnable target)
          分配新的 Thread 对象。

Thread(Runnable target,String name)
          分配新的 Thread 对象。

package com.bin.duoxiancheng;
public class D2 implements Runnable{
    int i=0;
    public void run(){
       for(i=0 ; i<50; i++){
           System.out.println(i);
           System.out.println(Thread.currentThread().getName());
           try {
              Thread.sleep(100);
           } catch (InterruptedException e) {
              // TODO Auto-generatedcatch block
              e.printStackTrace();
           }
       }
    }
    public static void main(String[] args){
       D2 d=new D2();
        Thread t=new Thread(d);
        t.start();
    }
}


线程中状态之间的转变

New状态,使用new创建的线程对象处于新建状态,仅仅在堆栈中分配了内存。

Runnable就绪状态。当线程调用start方法后,线程进入就绪状态。虚拟机会为他创建方法调用栈和程序计数器,处于这个状态的线程位于可运行池中,等待获得CPU使用权。

Running运行状态

  这个状态的线程正在占用CPU,如果一个计算机只有一个CPU,那么这个时刻会有一个线程处于这个状态,如多个CPU,可有多个线程占用不同的CPU,只有处于就绪状态的线程才有机会变为运行状态。

阻塞状态(Blocked)

 由于某些原因放弃CPU暂时停止运行,此时虚拟机不会给线程分配CPU,直到重新进入就绪状态,

   阻塞状态可分为以下3种。

1、 当处于运行状态中的某个对象执行了wait()方法,虚拟机就把这个线程放到这个对象等待池中。wait()是用来锁定一个对象的,在调用这个notify()方法前,他后面的代码永远不会被执行。这种锁定不能够自动解锁, 你必须在另一个线程中调用这个对象的notify()方法。

2、 当线程处于运行状态,试图获得某个对象的同步锁,若同步锁被其他线程占用,虚拟机会把这个线程放到这个对象的锁池中。

3、 当线程执行了sleep方法,或者调用join方法,或者发出了I/O请求时候,就会进入这个状态.

当线程执行System.out.println或者System.in.read()方法时,就会发出I/O请求,线程放弃cpu进入阻塞状态,直到I/O处理完成,

死亡状态(dead)

当线程退出run()方法时,就进入死亡状态,有可能正常 执行完run方法退出,也有可能异常退出,

Thread类中的isAlive()方法判断一个线程是否活着,当处于死亡状态或者新建状态时,该方法返回false,否则返回TRUE。

线程的调度

  虚拟机按特定的机制为CPU分配使用权,主要有两种调度模式,分时调度模型和抢占式调度模型。分时调度模型就是线程轮流让CPU获得使用权,平均分配线程。抢占式调度为让线程中优先度高的占用CPU,若优先级相同,那么就随机选择一个。处于运行状态的线程会由以下原因放弃CPU

   1、虚拟机转到就绪状态,其他线程获得机会。

   2、线程进入阻塞状态;

   3、线程运行结束

看以下程序:

package com.bin.duoxiancheng;
public class D3 extends Thread {
    public static int count=0;
    public static StringBuffer log=new StringBuffer();
    public void run(){
     for(int a=0;a<20;a++){
        log.append(currentThread().getName()+":"+a+" ");
        if(++count %10 ==0)log.append("\n");
     }
    }
    public static void main(String[] args) throws InterruptedException {
       // TODO Auto-generatedmethod stub
          D3 d1= new D3();
          D3 d2 =new D3();
          d1.setName("m1");
          d2.setName("d2");
          d1.start();
          d2.start();
          while(d1.isAlive()||d2.isAlive())
          Thread.sleep(500);
          System.out.print(log);
    }
}


结果为:

m1:0 m1:1 m1:2 m1:3 m1:4 m1:5m1:6 m1:7 m1:8 m1:9

m1:10 m1:11 m1:12 m1:13 m1:14m1:15 m1:16 m1:17 m1:18 m1:19

m2:0 m2:1 m2:2 m2:3 m2:4 m2:5m2:6 m2:7 m2:8 m2:9

m2:10 m2:11 m2:12 m2:13 m2:14 m2:15 m2:16m2:17 m2:18 m2:19

 

程序中m1先获得主动权,直到结束才交予m2,如果想让一个线程交给另外一个线程运行可以采取以下方法:

1、  调整优先级;调用Thread.setPriority(Thread.MAX_PRIORITY),参数取值范围为1-10,值越高优先度越高。

2、 调用Thread.sleep()方法;参数以毫秒为单位,当睡眠结束时候就会转为就绪状态,当另一个线程处于运行状态中,此线程在运行池 中处于等待状态

3、 调用Thread.yield()方法,如有有相同优先级别的线程处于就绪状态,那么yield把当前运行的线程放到可运行池中并使另一个线程运行;若没有相同级别的,则不运行,执行yield后,当前线程进入就绪状态,跟SLEEP不同,sleep为进入阻塞状态。

4、 调用JOIN方法;使用此方法后,当前程序进入阻塞状态。直至另一个程序结束才会恢复 运行。

  

package com.bin.duoxiancheng;
public class D5 extends Thread{
    public void run(){
       for(int a=0;a<50;a++){
           System.out.println(getName()+":"+a);
       }
    }
    public static void main(String[] args) throws InterruptedException {
       D5 d=new D5();
       d.setName("m1");
       d.start();
       System.out.println("main: joinmachine");
       d.join();
       System.out.println("main:end");
 
    }
}

System.out.println("main:end");这段程序要等到d这个线程结束后方执行。

也可以写作d.join(10)超过10毫秒恢复运行。

Timer类定时器的使用

 参数使用如下:

(1)Timer.schedule(TimerTask task,Datetime)安排在制定的时间执行指定的任务。
(2)Timer.schedule(TimerTask task,Date firstTime ,long period)安排指定的任务在指定的时间开始进行重复的固定延迟执行.
(3)Timer.schedule(TimerTask task,long delay)安排在指定延迟后执行指定的任务.
(4)Timer.schedule(TimerTask task,long delay,long period)安排指定的任务从指定的延迟后开始进行重复的固定延迟执行.  第一个参数是要操作的方法,第二个参数是要设定延迟的时间,第三个参数是周期的设定,每隔多长时间执行该操作。
(5)Timer.scheduleAtFixedRate(TimerTask task,Date firstTime,long period)安排指定的任务在指定的时间开始进行重复的固定
速率执行.
(6)Timer.scheduleAtFixedRate(TimerTask task,long delay,long period)安排指定的任务在指定的延迟后开始进行重复的固定速率执行.

public class EggTimer {  
    private final Timer timer = new Timer();
    private final int minutes;
    public EggTimer(int minutes) {
       this.minutes = minutes;
    }
    public void start() {
       timer.schedule(new TimerTask() {
           public void run() {
               playSound();
              // timer.cancel();
            }
           private void playSound() {
               System.out.println("Your egg is ready!");
                // Start a new thread to play a sound...
            }
       }, 0,minutes * 60 * 1000);//几分钟执行一次
    }
   public static void main(String[] args) {
       EggTimer eggTimer = new EggTimer(2);
       eggTimer.start();
    }
}


同步代码块

   为了保证线程能正常的执行原子操作,java引入了同步机制,在代码原子操作的程序代码前加上synchronized标记 ,这样,任何时刻只允许一个线程拥有这把锁,

   这样当加了同步标记后,其他待进入线程放到Stack对象锁池中,线程进入阻塞状态。加锁方式如下:

Public synchronized String pop(){…}

也可以写作

Public String pop(){

 Synchronized(this){…}

}

下面看一个消费者,创造者的例子

public class SyncTest {
    public static void main(String[] args) {
       Stack stack =new Stack("stack");
       Producer producer1=new Producer(stack,"producer1");
       Consumer consumer1=new Consumer(stack,"consumer");
    }
}
class Producer extends Thread{
       private Stack theStack;
       public Producer(Stacks,String name){
          super(name);
          theStack = s;
          start();
       }
       public void run(){
          String goods;
          for(int i=0;i<20;i++){
              goods="goods"+(theStack.getPoint()+1);
              theStack.push(goods);
              System.out.println(getName()+": push "+goods+" to "+theStack.getName());
              yield();
          }
       }
}
class Consumer extends Thread{
       private Stack theStack;
       public Consumer(Stacks,String name)
       {
          super(name);
          theStack=s;
          start();
       }
       public void run(){
          String goods;
          for(int i =0;i<20;i++){
              goods=theStack.pop();
              System.out.println(getName()+":pop "+goods+" from "+theStack.getName());
              yield();
          }
       }
}
    class Stack{
       private  String name;
       private String[] buffer=new String[100];
       int point = -1;
       public Stack(String name){this.name=name;}
       public String getName(){return name;}
       public int getPoint(){return point;}
       public synchronized String pop(){
           String goods=buffer[point];
           buffer[point]=null;
           Thread.yield();
           point --;
           return goods;
       }
       public synchronized void push(String goods){
           point ++;
           Thread.yield();
           buffer[point] =goods;
       }
    }


同步和并发问题

   同步锁解决资源共享资源竞争的有效手段,当一个线程已经操作共享资源,其他线程只有等待,只有当已经在操作共享资源的线程执行完毕后才能同步代码块,其他线程才能有机会共享。

以打水为例子,10个去打水,每人要打10桶,使用同步方式为,一个人打完10桶后,其他人才有机会打水,,当一个人打水,其他人必须等待,显然不合理。

public class Person extends Thread{
private Well well;
public Person (Well well){
    this.well=well;
    start();
}
public void run(){
    synchronized(well){
       for(int i=0;i<10;i++){
           well.withdraw();
           yield();
       }
      
    }
}
    /**
     * @param args
     */
    public static void main(String[] args) {
       Well well=new Well();
       Person persons[]=new Person[10];
       for(int i=0;i<10;i++){
           persons[i]=new Person(well);
       }
 
    }
 
}
class Well{
    private int water=100;
    public void withdraw(){
       water --;
       System.out.println(Thread.currentThread().getName()+": water left:"+water);
    }
}


则我们把同步锁加在withdraw(),则可以解决问题,例:

public class Person extends Thread{
private Well well;
public Person (Well well){
    this.well=well;
    start();
}
public void run(){
   
       for(int i=0;i<10;i++){
           well.withdraw();
           yield();
       }
}
    public static void main(String[] args) {
       Well well=new Well();
       Person persons[]=new Person[10];
       for(int i=0;i<10;i++){
           persons[i]=new Person(well);
       }
    }
}
class Well{
    private int water=100;
    public synchronized void withdraw(){
       water --;
       System.out.println(Thread.currentThread().getName()+": water left:"+water);
    }
}


线程安全类

一个线程安全类必须满足以下条件:

1、  这个类可以同时被多个线程安全访问。

2、  每个线程都能正常执行原子操作,得到正确结果。

3、  原子操作万仇,对象处于逻辑是合理状态。

  如上我们使用的Stack类就是可变类,point和buffer都可以发生变化,会造成共享资源的竞争。

线程之间的通信

不同线程之间执行不同的任务,如果任务有某种联系,必须实现通信功能,协调完成工作。

Java.lang.object类提供了用于线程通信的方法:

1、  wait(),该方式的线程释放对象锁,虚拟机把该对象放到等待池中,等待其他线程唤醒,并且释放所持有的对象的lock。

2、  notify(),该方法唤醒在等待池中的线程,虚拟机从等待池中随机选择一个线程.

3、  notifyAll()方法用于把对象池中所有的线程都转到对象锁池中。

例t1线程和t2线程可用wait和notify进行通信。

以编程的方式控制线程

实际编程中,一般是在受控的线程中定义一个标志量,其他线程通过改变标准变量的值,来控制线程的暂停、恢复运行已经自然终止,jdk1.2开始一些废弃了一些停止线程的方法

suspend() 使线程暂停,调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend().

resume() 暂停的线程恢复运行;

stop() 终止线程,它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在;

废弃的原因大概由于以上几个方法容易导致死锁等。

ThreadLocal类

 java.lang.ThreaLocal一般用来存放线程的局部变量,每个线程一般都有单独的局部变量,彼此 之间不会共享,该类主要有三个方法:

1、 public T get() 返回巨变线程的变量。

2、 protected T initialValue() 返回局部变量的初始值;

3、 public void set(T value) 设置当前线程的局部变量;

 其实看看TheadLocal的源码,我们可以发现他的思路,在类中有一个Map缓存,用于存储每个线程的局部变量,例:

package com.bin.duoxiancheng;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ThradLocal<T> {
  private Map<Runnable,T> values= Collections.synchronizedMap(new HashMap<Runnable,T>());
  public T get(){
	  Thread cuThread = Thread.currentThread();
	  T o=values.get(cuThread);
	  if(o ==null && !values.containsKey(cuThread)){
		  o=initValue();
		  values.put(cuThread, o);
	  }
	  return o;
  }
  public void set(T newValue){
	  values.put(Thread.currentThread(), newValue);
  }
 protected T initValue(){
	 return null;
 }
}


以上程序仅仅实现了总体思路,细节上ThreadLocal类实现的更健壮,还保证了线程结束后,从Map缓存中删除这个线程的局部变量的引用。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐