深入浅出让你了解什么是多线程

 

什么是多线程?

首先得知道什么是线程?

线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。 多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度。 多线程是多任务的特殊形式。通常,有两种类型的多任务:基于进程和基于线程的多任务。进程本质上是正在执行的程序。 因此,基于进程的多任务就是允许您的计算机同时运行两个或者更多程序的特性。例如,基于进程的多任务允许您在使用电子制表软件或者浏览Internet的同时运行文字处理程序。在基于进程的多任务中,程序是调度程序可以分派的最小代码单元。 多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。

 

多线程的创建方式

线程创建的四种方式:

  1. 使用Thread的子类 -----常用

  2. 使用Runnable接口 -----常用

  3. 使用匿名内部类直接定义:Thread t = new Thread(){ @Override public void run(){...}}; ---很少用

  4. 直接使用Thread -----(虽然可以创建,但是没意义)。

 

第一种方式

代码:

class Mythread extends Thread{
    @Override
    public void run(){
        System.out.println("this :"+this);
        System.out.println("Thread.currentThread:"+Thread.currentThread());
    }
}
public class practice2 {
   public static void main(String[] args){
       Mythread mythread = new Mythread();
       mythread.start();
   }
}

运行:

 

第二种方式

这种方式是在Java中推荐使用的,因为经常使用,所以详谈,在API文档中是这么说的:

Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了 Runnable。激活的意思是说某个线程已启动并且尚未停止。

此外,Runnable 为非 Thread 子类的类提供了一种激活方式。通过实例化某个 Thread 实例并将自身作为运行目标,就可以运行实现 Runnable 的类而无需创建 Thread 的子类。大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。

运用:

class Test implements Runnable{
     private int count = 100;
     @Override 
     public void run(){
         while(true){
         synchronized(this){
                 try {
                    Thread.sleep(400); //为了方便查看,这里给线程睡眠一下,不然时间太短一下就执行完了看不了效果
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
             count -=10;
             if(count<0)break;
             else{
                 System.out.println(Thread.currentThread().getName()+"还剩次数为:"+count);
             }
           }
         }
     }
 }
public class demo1 {
   public static void main(String[] args){
       Test test  = new Test();
       Thread t1 = new Thread(test,"线程1");
       t1.start();
       Thread t2 = new Thread(test,"线程2");
       t2.start();
   }
}
 

运行结果:

如果把第5行代码与第6行代码互换一下位置,如:

class Test implements Runnable{
     private int count = 100;
     @Override 
     public void run(){
         synchronized(this){
             while(true){
                 try {
                    Thread.sleep(400); //为了方便查看,这里给线程睡眠一下,不然时间太短一下就执行完了看不了效果
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
             count -=10;
             if(count<0)break;
             else{
                 System.out.println(Thread.currentThread().getName()+"还剩次数为:"+count);
             }
           }
         }
     }
 }
public class demo1 {
   public static void main(String[] args){
       Test test  = new Test();
       Thread t1 = new Thread(test,"线程1");
       t1.start();
       Thread t2 = new Thread(test,"线程2");
       t2.start();
   }
}

运行结果:

解析:

因为 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,具体什么时候运行是CPU来决定的,一旦得到spu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。又因为我们加了synchronized()同步锁,线程要依次执行,就绪状态的要排队等在synchronized的前面,如图:

 

为什么实现Runable的接口实现类要把对象传递给Thread才能启动?

因为实现Runable的接口类并不是Thread的对象,并不能开启线程,只能在创建线程的时候把这个实现类作为参数传递过去,在线程内部就会判断传进去的target(该实现类)是否为空,不为空的话说才去调用实现类的里面的run方法,所说就必须要把接口实现类传递给Thread才能开启线程。

在实现Runnable的接口类中this与Thread.currentThread()的区分别指什么?

释例代码:

//test
class Test implements Runnable{
    public void run(){
        System.out.println("Thread.currentThread:"+Thread.currentThread());
        System.out.println("this :"+this);
    }
}
public class practice1 {
    public static void main(String[] args){
    //create test instance
    Test test = new Test();
    //创建一个线程对象
    Thread t = new Thread(test,"线程1");
    //开启线程
    t.start();
    }
}

运行结果:

很显然:当前的this指的是test这个对象,而Thread.currentThread()指的是t这个线程对象,并且再想想, .currentThread()这个方法是个静态方法,类方法也不可能是this对象调用的,因为它们都不属于同一类。

 

同步锁中的静态函数与非静态函数的锁定对象的分别是?

知识点:

1、如果是一个非静态的函数,同步函数的锁对象就是调用方法的对象(this对象),如果是一个静态函数同步函数的锁对象是当前函数所属类的字节码文件(.Class对象)

2、同步锁的锁定的对象全局必须是唯一的,不然添加锁就没意义。

代码如下:

/**
 * 思路:
 * 创建三个线程,count = 5000,来执行count-=10;把每次的结果都存在arrayList的可变列表中,每次进来都判断是否已经慧慧,如
 * 果存在就打印"该对象已经存在",如果没有count到零时就正常退出。注意:程序有可能(极少)抛出异常,还不完善,但不影响结果!!仅作测试用!
 */
import java.util.ArrayList;
//验证同步函数与静态函数的锁对象
class Test implements Runnable{
    private static int count = 5000;
    //创建一个可变的列表,将count的值每次都加入,每次相加前都判断,有重复的就终止程序!!
    static ArrayList arrayList = new ArrayList(); 
    static boolean flag = false; //等于零时就退出
    public void run(){
        while(true){
            show();
            synchronized(this){
                if(arrayList.indexOf(new Integer(count))!=-1){
                    System.out.println("该对象已经存在!!!");
                }
                if(flag)return;
                if(count==0){
                    flag = true;
                    return;
                }
                arrayList.add(new Integer(count));
                count -= 10;
            }
        }
    }
    public synchronized  void show(){  //同步函数
        if(arrayList.indexOf(new Integer(count))!=-1){
            System.out.println("该对象已经存在!!!");
        }
        if(flag)return;
        if(count==0){
            flag = true;
        }
        arrayList.add(new Integer(count));
        count -= 10;
    }
}
public class practice1 {
    public static void main(String[] args){
    //create test instance
    Test test = new Test();
    //创建一个线程对象
    Thread t1 = new Thread(test,"线程1");
    //开启线程
    t1.start();
    //同上
    Thread t2 = new Thread(test,"线程2");
    t2.start();
    Thread t3 = new Thread(test,"线程3");
    t3.start();
    }
}

不管运行多少遍,永远都没有重复的元素,如:

将30行的代码改成public synchronized static void show(){ 如下:

1 public synchronized static void show(){ //同步函数

运行:

说明此时线程不是同步的了,锁的并不是this这个对象,将16行的代码改成:synchronized(Test.class){ 如:

synchronized(Test.class){ //synchronized(this.getClass){ // 也可以这样获取当前的.class类对象,

运行了好:

实际上java的每个类被编译成.class文件的时候,java虚拟机(叫jvm)会自动为这个类生成一个类对象,这个对象保存了这个类的所有信息(成员变量,方法,构造器等),以后这个类要想实例化(也就是创建类的实例或创建类的对象)那么都要以这个class对象为蓝图(或模版)来创建这个类的实例。例如 class<?> c=Class.forName("com.pojo.User"); c就是User的类对象,而 User u=new User();这个u就是以c为模版创建的,其实就相当于u=c.newInstance();

结论:

如果是一个非静态的函数,同步函数的锁对象就是调用方法的对象(this对象),如果是一个静态函数同步函数的锁对象是当前函数所属类的字节码文件(.Class对象)

 

第三种方式

因为少用,所以只介绍怎么用,代码:

/**
 *使用匿名内部类创建 一个线程
 */
public class practice2 {
   public static void main(String[] args){
      Thread thread = new Thread(){
          @Override
          public void run(){
              System.out.println(Thread.currentThread().getName());
              System.out.println(this);
          }
      };
      thread.start();
   }
}

运行:

第四种方式

public class demo1 {
   public static void main(String[] args){
       System.out.println(Thread.currentThread());
       Thread t = new Thread(); //创建一个线程对象
       t.start();//开启当前的子线程
       System.out.println(t); //打印子线程对象
   }
}

运行:

其中:Thread[main,5,main]

第一个参数:表示线程的名称,如果没有给定的话是系统默认的,如main、Thread-0(从0开始的都子线程)

第二个参数:表示线程的优先级,默认是5

第三个参数: 表示当前线程是在主线各中输出的

run方法在默认情况下是这样实现的:

@Override public void run() { if (target != null) { target.run(); } }

所以如果你直接Thread thread = new Thread();创建对象,语法上没有错的,也是在子线程运行的,但是run()方法是系统默认的,也不是你自己想定义的线程中的方法,那这个线程有什么意义呢?

 

多线程中的start()方法与run方法的区别?

Start:

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到spu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

Run:

run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行线程的启动是比较复杂的,需要为线程分配资源,它的START方法被调用时系统才会为线程分配资源。调用线程的run方法只能算普通的方法调用一样,得运行完run里面的代码整个程序才能往下进行,而如果调用start方法,线程和MAIN方法就会 抢资源,打印的语句会交替出现,

start()的源代码:

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

线程的运行不是像其他方法那么简单的额调用一下就运行的。想要运行一个线程,肯定是要和操作系统打交道,告诉操作系统我要添加一个线程,run方法只是个简单的方法,里面就是你要运行的代码。 start方法里面会调用一个native方法,这个方法才是真正和操作系统交流的

同步锁中的单例设计模式

分两种,分别是:

1、饿汉模式 2、懒汉模式(这里只介绍懒汉,因为面试经常问,但不常用,因为线程不安全的,所以这里说该怎么改成线程安全的.)

平常写的懒汉模式代码是这样的,然后我们加入三个线程,看看效果,如:

/**
 * 单例设计模式:懒汉模式 
 */
class Singleton{
    private static Singleton s = null;
    public static Singleton getInstance(){
        if(s==null){
            s = new Singleton();
        }
        return s;
    }
}
class Custom implements Runnable{
    public void run(){
        Singleton s = Singleton.getInstance();
        System.out.println("Thread.currentThread():"+Thread.currentThread().getName()+"--Singleton:"+s);
    }
}
public class test {
public static void main(String[] args){
       Custom t = new Custom();
       Thread thread1 = new Thread(t,"线程1");
       Thread thread2 = new Thread(t,"线程2");
       Thread thread3 = new Thread(t,"线程3");
       thread1.start();
       thread2.start();
       thread3.start();
   }
}
/**
 * 单例设计模式:懒汉模式 
 */
class Singleton{
    private static Singleton s = null;
    public static Singleton getInstance(){
        if(s==null){
            s = new Singleton();
        }
        return s;
    }
}
class Custom implements Runnable{
    public void run(){
        Singleton s = Singleton.getInstance();
        System.out.println("Thread.currentThread():"+Thread.currentThread().getName()+"--Singleton:"+s);
    }
}
public class test {
public static void main(String[] args){
       Custom t = new Custom();
       Thread thread1 = new Thread(t,"线程1");
       Thread thread2 = new Thread(t,"线程2");
       Thread thread3 = new Thread(t,"线程3");
       thread1.start();
       thread2.start();
       thread3.start();
   }
}

运行:

所以为了安全起见,将代码加上synchronized(),如:

/**
 * 单例设计模式:懒汉模式 
 */
class Singleton{
    private static Singleton s = null;
    public static Singleton getInstance(){
        synchronized ("lock") { //加上同步锁后,就安全了,但是效率变低了
         if(s==null){
            s = new Singleton();
        }
        return s;
      }    
   }
}
class Custom implements Runnable{
    public void run(){
        Singleton s = Singleton.getInstance();
        System.out.println("Thread.currentThread():"+Thread.currentThread().getName()+"--Singleton:"+s);
    }
}
public class test {
public static void main(String[] args){
       Custom t = new Custom();
       Thread thread1 = new Thread(t,"线程1");
       Thread thread2 = new Thread(t,"线程2");
       Thread thread3 = new Thread(t,"线程3");
       thread1.start();
       thread2.start();
       thread3.start();
   }
}

不管运行多少遍,都是打印同一个地址:

上面代码虽然可以,但是效率太低,也是面试被经常问到的,为了提高效率,得加个双重if判断,以下代码才是最终版本:

/**
 * 单例设计模式:懒汉模式 
 */
class Singleton{
    private static Singleton s = null;
    public static Singleton getInstance(){
        if(s==null){
         synchronized ("lock") { //加上同步锁后,就安全了,但是效率变低了
           if(s==null){
            s = new Singleton();
         }
        }    
      }
        return s;
    }
}
class Custom implements Runnable{
    public void run(){
        Singleton s = Singleton.getInstance();
        System.out.println("Thread.currentThread():"+Thread.currentThread().getName()+"--Singleton:"+s);
    }
}
public class test {
public static void main(String[] args){
       Custom t = new Custom();
       Thread thread1 = new Thread(t,"线程1");
       Thread thread2 = new Thread(t,"线程2");
       Thread thread3 = new Thread(t,"线程3");
       thread1.start();
       thread2.start();
       thread3.start();
   }
}

sleep()与wait()的区别?

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。

 1. setPriority( ): 设置线程的优先权;

2. yield( ): 暂停线程的执行,给其它具有相同优先权的线程执行的机会,若此时没有其它线程执行,则此线程继续执行。这个函数并不会释放锁住的对象。

 3. join( ): 等待加入的线程执行完毕才会执行下一个线程。加入的线程wait()后需要通过interrupt( )来唤醒。

 4. wait( ): 类似sleep( ), 不同的是,wait( )会先释放锁住的对象,然后再执行等待的动作。注意,这个函数属于Object类。另外,由于wait( )所等待的对象必须先锁 住,因此,它只能用在同步化程序段或者同步化方法内,否则,会抛出异常IllegalMonitorStateException.

5.notify();唤醒 随机唤醒线程池中的一个线程。 notifyAll(); 唤醒所有等待的线程

wait和notify的使用注意点 : 1.wait方法和notify方法是属性Object对象 2.wait方法和notify方法必须在同步线程中执行 3.wait方法和notify方法必须有锁对象来调用

 

为什么wait()、notify()、等对象在Object类中定义,而不在Thread中?

因为锁是任意对象,一个程序中可以有多个同步锁,a对象的所属的线程需要wati()/notify是靠a对象来调用的。也就是是说哪个对象的线程等待了,就行用哪个对象来唤醒,其他对象不能唤醒。

不知大家有没有和我一样想过这样的一个问题?假设有两条线程,如果当前的线程不等待,直接唤醒另一条已经在处于等待状态的线程,那么到底会运行哪个线程呢?

还是运行当前的线程,虽然另一条线程被唤醒了,只是表明它处于就绪状态,但是仍无法获得obj锁。直到当前线程wait()后退出synchronized块,释放obj锁后,另一条线程才会执行。

 

如何停止线程(中断)线程?

stop方法现在已经过时了。在API中文档中的是这么说的:

既然stop()方法不能使用了,那如何停止线程? 所以只有一种,run方法结束。 开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。

注意特殊情况: 当线程处于了冻结状态(也就是调用了sleep()、wait()后)。就不会读取到标记。那么线程就不会结束。

当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。 强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。

Thread类提供该方法 interrupt()与interrupted(),以下是两者的区别:

interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。我们调用sleep、wait等此类可中断(throw InterruptedException)方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被jvm自动清除了,就是说我们调用该线程的isInterrupted 方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。这样做的原因是,java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位,所以要清除!。

interrupted是静态方法,返回的是当前线程的中断状态。例如,如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。

总结:如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

 

condition与Lock接口(前者替代了监视器方法、后者替代了synchronized())

查看API文档介绍:

/*
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。

Lock:替代了Synchronized
    lock 
    unlock

创建一个Condition对象
lock.newCondition()     //其中lock是Lock的实例

Condition:替代了Object wait notify notifyAll
    await();     等待
    signal();    唤醒
    signalAll();  全部唤醒

    好处:在多个线程访问同一个资源的情况下,如多个生产者与消费者,如果是使用notifyAll的话就全部唤醒了在当前等待的线程,
    这样虽然可以,但是效率过低,Condition可以直接唤醒任何一方
*/
发布了83 篇原创文章 · 获赞 31 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/lonely_bin/article/details/100382355