线程详解笔记

目录

一、进程与线程

1、什么是进程?什么是线程?

进程:

线程:

2、进程和线程是什么关系?

3、进程和线程的区别

4、关于线程对象的生命周期?

二、实现线程的两种方式

1、概述实现线程的两种方式

2、Thread类(创建线程方式一)

3、实现Runnable接口(创建线程方式二)

4、Thread和Runnable的区别

5、匿名内部类方式实现线程的创建

三、实现线程的第三种方式---实现Callable接口(有返回值)

四、线程阻塞的四种方法

1、通过Sleep()进行线程阻塞,通过interrupt()进行唤醒

2、通过join()对线程进行阻塞

3、通过yield()方法进行线程阻塞

4、通过wait()进行线程阻塞,通过notify()进行线程唤醒

五、线程中的异常捕捉

六、如何合理地终止一个线程的执行?----打标记

七、守护线程

八、死锁

九、线程安全

十、生产者与消费者模型

十一、线程池


更新中~~~

一、进程与线程

1、什么是进程?什么是线程?

进程:

       进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志。

进程具有的特征:

动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;

并发性:任何进程都可以同其他进行一起并发执行;

独立性:进程是系统进行资源分配和调度的一个独立单位;

结构性:进程由程序,数据和进程控制块三部分组成;

线程:

       在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

Java程序中至少有两个线程(1、main方法的主线程;2、垃圾回收线程)

线程具有的特征:

1、原子性:即一个操作或多个操作要么全部执行并且在执行过程中不被任何因素打断,要么就不执行

package com.wdy.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadAtomicDemo {
   
	public static int i=0;

	public static class AtomicThread extends Thread{ //静态内部类继承Thread
		@Override
		public void run() {
			while(true) {
				i++;//i++,i--,++i,--i 不是原子性操作。
				//比如i++ 先赋值,后自增两个步骤,中间有间隔。这个两个步骤可能出现在两个线程中。因此结果中出现了两个连续重复的i
				//因此这样的操作是线程不安全的。
						
				System.out.println(this.getName()+"::"+i);
				try {
					sleep(200);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		Thread t1=new AtomicThread();
		t1.setName("A");
		Thread t2=new AtomicThread();
		t2.setName("B");
		Thread t3=new AtomicThread();	
		t3.setName("C");
		
		t1.start();
		t2.start();
		t3.start();	
	}
}

i++,i–,++i,–i 不是原子性操作。比如i++ 先赋值,后自增两个步骤,中间有间隔。这个两个步骤可能出现在两个线程中。因此结果中出现了两个连续重复的i。因此这样的操作是线程不安全的。执行结果:

使用AtomicInteger类创建原子性的对象,i.incrementAndGet()方法进行自增,保证原子性,线程安全

package com.wdy.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadAtomicDemo {
   
	public static int i=0;

	public final static AtomicInteger i=new AtomicInteger(0);
	
	public static class AtomicThread extends Thread{ //静态内部类继承Thread
		@Override
		public void run() {
			while(true) {
			i++;
		System.out.println(this.getName()+"::"+i.incrementAndGet());
				try {
					sleep(200);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		Thread t1=new AtomicThread();
		t1.setName("A");
		Thread t2=new AtomicThread();
		t2.setName("B");
		Thread t3=new AtomicThread();	
		t3.setName("C");
		
		t1.start();
		t2.start();
		t3.start();	
	}
}

 同理:

Java线程原子类:

基本类型:

AtomicBoolean、AtomicInteger、AtomicLong

数组:

AtomicIntegerArray、AtomicLongArray、AutomicReferenceArray

引用:

AtomicReference、AtomicReferenceFieldUpdate、AtomicMarkableReference

字段:

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference

2、可见性:当多个线程同时访问内存中一个参数的时候,此时两个线程获得的参数是内存中的参数备份,此时一个线程修改了该参数另一个线程是感知不到的,可见性就是将不同线程的参数备份保持一致。

Volatile关键字就是用于保证内存的可见性,当线程A更新了volatile修饰的变量时,它会立即刷新到主内存中,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。使用volatile关键字修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存

package com.wdy.thread;
public class ViasbleDemo01 {
     public static volatile boolean f=false;
   
     public static class A extends Thread{
    	 @Override
    	public void run() {  //如果f=true,执行循环
    		while(true) {
    			if(f) {
    				System.out.println(getName()+"运行:"+f);
    			break;
    			}
    		}
			}   	
     }
     
     public static class B extends Thread{
    	 @Override
    	public void run() {
    		try {
				sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
    		f=true;
    		System.out.println(getName()+"运行"+f);
    	}
     }
     
     public static void main(String[] args) {
		Thread t1=new A();
		t1.setName("A");
		Thread t2=new B();
		t2.setName("B");
		
		t1.start();
		t2.start();	
	}
}

有A、B两个线程,A线程如果f为true相当于一个死循环一直循环,而B线程在运行时就,将f赋值为flase停止A线程的运行,在f这个变量还没有被volatile关键字修饰时,理想状态应该时B线程一旦抢到CPU时间片运行后A线程中f变量的值被赋值为flase停止运行,然而事实结果并不是这样,A线程的f变量仍然为true,这是由于B线程修改了A线程中的参数,而A线程并没有感知到,线程之间不可见,通过volatile关键字将f变量修饰后,A、B线程可见,B修改了A中的变量,A能够马上感知到。 

3、有序性:

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在多个线程运行时,不要调整内部指令执行的顺序。

package com.wdy.thread;
/*
 * 单例模型
 */
public class OrderThreadDemo {	
	private static volatile OrderThreadDemo instance = null;
    //添加volatile的作用;解决可变性,保证应用的有序性(按第一种方式),
	private OrderThreadDemo() {}
	
	public static OrderThreadDemo getInstance() {
	/**
	*假设有两个线程进入方法
	*if instance == null	
	* 准备创建对象 instance = new OrderThreadDemo();
	*可能分为三个流程:第一种方式
	*1.开辟内存空间
    *2.初始化对象
	*3.引用和对象绑定
	*
	*
	另外一种方式:
	*1.开辟内存空间
	*2.引用和对象绑定instance--->对象, instance不是null
	*3.初始化对象(未完成)
	*/    
		
	if(instance==null) { //判断OrderThreadDemo是否存在
		instance=new OrderThreadDemo();
	}
	return instance;
	}
}

2、进程和线程是什么关系?

一个进程可以启动多个线程。

3、进程和线程的区别

1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;

3. 进程之间相互独立,但同一进程下的各个线程之间堆内存和方法区内存共享,栈内存独立,一个线程一个栈。

4. 调度和切换:线程上下文切换比进程上下文切换要快得多

4、关于线程对象的生命周期?

    新建状态:采用new语句创建完成
    就绪状态:执行start()后就进入了就绪状态,拥有了抢夺CPU时间片(就是执行权)的权利,当                        抢夺到CPU时间片后会执行run()进入运行状态
    运行状态:执行run()后标志着这个线程进入了运行状态,当之前占有的CPU时间片用完之后,                        会重新回到就绪状态继续抢夺CPU的时间片,当再次抢到CPU的时间片后,会重新                        执行run(),接着上一次的代码继续执行
    阻塞状态:执行了wait语句、sleep语句或等待某个对象锁,等待输入的场合等,此事先猜金融                          阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片
    死亡状态:退出run()方法

二、实现线程的两种方式

java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。

1、概述实现线程的两种方式

第一种方式】编写一个类,直接继承java.lang.Thread,重写run方法。

// 定义线程类
		public class MyThread extends Thread{
			public void run(){			
			}
		}
		// 创建线程对象
		MyThread t = new MyThread();
		// 启动线程。
		t.start();

第二种方式】 编写一个类,实现java.lang.Runnable接口,实现run方法。

//定义一个可运行的类
        public class MyRunnable implements Runnable {
            public void run(){            
            }
        }
        // 创建线程对象
        Thread t = new Thread(new MyRunnable());
        // 启动线程
        t.start(); 

注意:

第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。 

图示:

分析:

   程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()方法调用的时候被创建,随着调用mt的对象的start方法,另外一个新的线程也启动了,这样这个应用就在多线程下运行。多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所有的栈内存空间,进行方法的压栈和弹栈。(使用了多线程机制之后, main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈)

       当执行线程的任务结束了,线程自动在栈内存中释放了,但是当所有的执行线程都结束了,那么进程就结束了。 

2、Thread类(创建线程方式一)

Java.lang.Thread类,API中该类定义了有关线程的一些方法,具体如下:

构造方法:

   Public Thread(); 分配一个新的线程对象

   Public Thread(String name); 分配一个指定名字的新的线程对象

   Public Thread(Runnable target); 分配一个带有指定目标新的线程对象

   Public Thread(Runnable target,String name); 分配一个带有指定目标新的线程对象并指定名字

【代码演示】

public class MyThread extends Thread {
      // 重写run方法 完成该线程的执行逻辑     
    public void run(){
        for(int i=0;i<20;i++){
            System.out.println(i);
        }
    }
}
测试类:
public class Demo01 {
    public static void main(String[] args) {
        //创建自定义线程对象
        MyThread mt = new MyThread();
        //mt.run()  这个方法不会启动线程,相当于调用.run()方法
        //开辟一个新的栈空间,启动新线程
        mt.start();  //这个方法瞬间结束,且这段代码运行完成后才会执行下面的代码,然后主线程与分支线程同时执行

        //在主方法中执行for循环
        for(int i=0;i<20;i++){
            System.out.println(i);
        }
    }
}

常用方法:

   Public String getName();获取当前线程名字

   Public void setName();设置当前线程的名字

当线程没有设置名字时,默认的名字有什么规律?

Thread-0

Thread-1

Thread-2

........

   Public void start(); 导致此线程开始执行,java虚拟机调用此线程的run方法

   Public void run(); 此线程要执行的任务在此处定义代码

   Public static void sleep(long millis); 使当前在执行的线程以指定的毫秒数暂停(暂时停止执行)

   Public static Thread currentThread();返回当前正在执行的线程对象的引用。 

3、实现Runnable接口(创建线程方式二)

Java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可

步骤如下:

  1. 定义runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
  2. 创建runnable实现类的实例,并一次实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start方法来启动线程
//这并不是一个线程类,只是一个可运行的类
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<20;i++){            
           System.out.println(Thread.currentThread().getName()+""+i);
           //Thread.currentThread()方法是调用Thread类中的静态方法currentThread()获取当前线程对象
        }
    }
}
//测试类
public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread t = new Thread(mr, "xiao");
        //启动新线程
        t.start();
        for (int i=0;i<20;i++){
            System.out.println("da"+i);
        }
    }
}

       通过实现runnable接口,使得该类有了多线程类的特征,run方法是多线程程序的一个执行目标,所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

       在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)创建出对象,然后调用Thread对象的start方法来运行多线程代码。

      实际上所有的多线程代码都是通过运行Thread的start方法来运行的,因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是要通过Thread类的对象,来控制线程的。

注意:

   Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run方法仅仅作为线程执行体,而实际的线程对象依然是Thread类的实例,只是该Thread线程负责执行其target的run方法。

4、Thread和Runnable的区别

       如果一个类继承Thread类,则不适合资源共享,但是如果实现了Runnable接口的话,则很容易实现资源共享。

   总结:实现Runnable接口比继承Thread类所具有的优势有:

  1. 适合多个相同的程序代码的线程去共享一个资源
  2. 可以避免java中的单继承的局限性
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程相对独立了
  4. 线程池只能放入实现了Runnable接口或者Callable的类线程,不能直接放入继承Thread的类的线程。

注意: 

      在java中,每次程序执行至少要启动两个线程:一个main线程,一个垃圾回收机制 

5、匿名内部类方式实现线程的创建

     使用线程的匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。使用匿名内部类的方式实现runnable接口,重写runnable接口中run方法即可。

【代码演示】

public class NoNameInnerClassThread {
    public static void main(String[] args) {
        Runnable r = new Runnable(){
            public void run(){
                for(int i=0;i<20;i++){
                    System.out.println("李四"+i);
                }
            }
        };
        new Thread(r).start();
        //主线程
        for (int i=0;i<20;i++){
            System.out.println("张三"+i);
        }
    }
}

三、实现线程的第三种方式---实现Callable接口(有返回值)

       这种方式实现的线程是JDK8新特性,可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。

优点:可以获取到线程的执行结果

缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

【代码演示】

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask; // JUC包下的,属于java的并发包,老JDK中没有这个包
public class ThreadTest{
    public static void main(String[] args){

//第一步:创建一个"未来任务类"对象。
//参数非常重要,需要给一个Callable接口实现关对象。
      FutureTask task = new FutureTask(new Callable(){
        public Object call() throws Exception{  //call()方法相当于run方法, 只不过这个方法有返回值
        //线程执行一个任旁,执行之后可能会有一个执行结果
        //模拟执行
             System.out.println("call method begin")
             Thread.sleep(millis: 10 * 10);
             System.out.println("call method end!");
             int a=100;
             int b=200; 
             return a+b; //自动装箱(int变成Integer)
            }
});

       //创建线程对象
       Thread t = new Thread(task);
       //启动线程
       t.start();

      //这里是main方法。这是在主线程中。
      //在主线程中,怎么获取t线程的返回结果? get()方法的执行会导致“当前线程阻塞”
       Object obj = task.get();
       System.out.println("线程执行结果:"+ obj);

      // main方法这里的程序要想执行必须等get()方法的结束,而get()方法可能需要很久。
      //因为get()方法是为了拿另一个线程的执行结果,另一个线程执行是需要时间的。
       System.out.print1n("he11o wor1d!"); 
  }
}

四、线程阻塞的四种方法

1、通过Sleep()进行线程阻塞,通过interrupt()进行唤醒

1、它是一个静态方法:Thread.sleep(毫秒数)

2、参数是毫秒

3、作用:让当前线程进入睡眠,进入阻塞状态,放弃站有的CPU时间片,让给其他线程使用

4、注意:因为它是一个静态方法,所以它如果在主线程中通过线程对象调用,在执行时还是会转换为Thread.sleep(),然后主线程进行休眠,如果要让分支线程进行休眠,需要在run()方法中通过Thread.sleep()进行休眠

public class Test{
   public static void main(String[] args){
       MyThread t=new MyThread();
       /*
        *因为sleep()是静态方法,所以在执行时会转换为Thread.sleep(),
        *然后主线程休眠5秒(而不是分支线程休眠5秒)
        */
       try{
            t.sleep(1000*5);
         }catch(InterruptException e){
            e.printStackTrace();
          }

         t.start();//开启新线程

     }
  
   class MyThread extends Thread{
          public void run(){
             //逻辑代码
            }
     }

如何唤醒被sleep()方法阻塞的进程?

在主线程中通过线程对象调用interrupt()方法

public class Test{
   public static void main(String[] args){
       MyThread t=new MyThread();

       t.start();//开启新线程
       
        //主线程休眠5秒,然后醒来 
       try{
            Thread.sleep(1000*5);
         }catch(InterruptException e){
            e.printStackTrace();
          }
       //终止t线程睡眠
       t.interrupt();
     }
  
   class MyThread extends Thread{
          public void run(){
            //休眠一年
             try{
               Thread.sleep(1000*60*60*24*365);
            }catch(InterruptException e){
               e.printStackTrace();
            }
               
     }

实质是t.interrupt()导致分支线程对象t中的线程睡眠Thread.sleep(1000*60*60*24*365)报错,然后catch捕捉异常,睡眠结束

2、通过join()对线程进行阻塞

1、它是一个实例方法

2、作用:如果有两个线程,在A线程调用启动方法后再调用join()方法进行阻塞,那么B线程会等A线程运行完后再进行运行

public class Test{
   public static void main(String[] args){
       Thread t1=new MyThread();
       t1.setName("A");
       Thread t2=new MyThread();
       t1.setName("B");
       t1.start();//开启新线程

       t1.join();//线程阻塞
       
       t2.start();//开启新线程
      
     }
  
   class MyThread extends Thread{
          public void run(){
            for(int i=0;i<3;i++){
                   System.out.println(this.getName()+"----"+i);
              }
          } 
     }

运行结果应该是:

A-----1

A-----2

A-----3

B-----1

B-----2

B-----3

3、通过yield()方法进行线程阻塞

1、它是一个静态方法

2、作用:让当前线程放弃CPU资源,阻塞,给其他线程机会运行,当前线程进入就绪状态然后跟其他的线程再一起抢夺CPU资源

public class Test{
   public static void main(String[] args){
       MyThread t=new MyThread();

       t.setDaemon(true); //在启动线程之前,将线程设为守护线程
      
       t.start();//开启新线程
       
      //主线程为用户线程
      for(int i=0;i<10;i++){
        System.out.println("主线程-----"+i);
        }
     }
  
   class MyThread extends Thread{
          public void run(){
            for(int i=0;i<10;i++){
                if(i%2==0){
                  yield(); //阻塞当前线程,释放CPU资源进入就绪状态与其他线程再进行CPU资源抢夺
                 }
                   System.out.println("分支线程-----"+i);
              }
          } 
     }
               

4、通过wait()进行线程阻塞,通过notify()进行线程唤醒

1、该方法不是Thread类中的方法,而是Object类中的方法,所以不能用线程对象去调用它

2、作用:

Object  oj=new Object(); 

oj.wait();

让正在oj对象上活动的线程进入等待状态,无限期等待直至被唤醒为止;会释放oj对象上占有的锁

五、线程中的异常捕捉

在线程这个类中的run()方法当中的异常不能用throws直接抛出,只能够通过try...catch进行异常捕捉,因为run()在Thread这个父类中未抛出任何异常,子类不能比父类抛出更多的异常,所以不能用throws进行异常抛出

六、如何合理地终止一个线程的执行?----打标记

如何不通过睡眠的形式,合理的终止一个线程的执行呢?我们可以在线程中定义一个boolean变量并且赋值为true,如果需要终止这个线程则在主线程中将这个boolean型变量赋值为false,具体示例如下:

public class Test{
   public static void main(String[] args){
       MyThread t=new MyThread();

       t.start();//开启新线程
       
        //主线程休眠5秒,然后醒来 
       try{
            Thread.sleep(1000*5);
         }catch(InterruptException e){
            e.printStackTrace();
          }
       //终止t线程睡眠
       t.run=flase;
     }
  
   class MyThread extends Thread{
          //声明一个boolean属性作为标记
          boolean run=true;

          public void run(){
           if(run){
             //线程执行代码
           }else{
             //在这里保存还未保存的
             //终止当前线程
             return;
           }
     } 

七、守护线程

Java语言中线程主要分为两大类:用户线程和守护线程(又称后台线程,代表性的是”垃圾回收线程“)

特点:一般守护线程是一个死循环,但只要所有的用户线程结束,守护线程自动结束

实现:在主线程中调用start()之前,通过线程对象.setDaemon(true)将线程设为守护线程

public class Test{
   public static void main(String[] args){
       MyThread t=new MyThread();

       t.setDaemon(true); //在启动线程之前,将线程设为守护线程
      
       t.start();//开启新线程
       
      //主线程为用户线程
      for(int i=0;i<10;i++){
        System.out.println("主线程-----"+i);
        }
     }
  
   class MyThread extends Thread{
          public void run(){
             int i=0;
             while(true){  //死循环
               System.out.println("守护线程------"+(++i));
             }
          } 
     }
               

八、死锁

九、线程安全

在多线程情况下保证数据的一致性和准确性

1、使用Java提供的原子类

2、使用synchronized关键字

3、使用ReetrantLook锁

十、生产者与消费者模型

十一、线程池

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值