多线程技术讲解---持续更新

多线程技术讲解

一、传统线程技术的回顾

1. 多线程的创建(两种)

  1. 通过创建线程对象,复写run(),通过start()调用该线程。

本质是继承

Thread thread = new Thread(){
     	@Override
        public void run() {...}
    };
 thread.start();
  1. 通过有参构造创建线程对象,复写run(),通过start()调用该线程。

本质是接口实现Runnable

 Thread  thread2 = new Thread(new Runnable() {
           @Override
           public void run() {...}
 });
thread2.start();
  • 源码
  • 首先进入构造方法中看见init方法
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
通过对成员变量的target的赋值,就能实现多线程。

2. 线程池的创立

传统的线程创建,寿命只有从线程对象的确定,以及到线程代码的结束,如果一个程序需要不同的时间,创建大量不同的线程,用上述的两种方式创建线程,太过于消耗内存资源。由此引入新的的技术

  • 线程池技术

线程池的创立都是通过Executors创建,通过代理实现不同类型线程池的创建,
线程池都是通过不同的队列的实现,(LinkedBlockingQueue,SynchronousQueue)

//        固定线程数
 ExecutorService threadPool = Executors.newFixedThreadPool(3);
//        根据任务调度线程数(首选,当它回收旧线程时,停止创建新线程,在使用出问题时,才使用newFixedThreadPool)
 ExecutorService threadPool = Executors.newCachedThreadPool();
//       单线程池像是newFixedThreadPool(1);在处理单线程同步问题的时候,可以减少对同步复杂性,因为只有一个线程执行,就不存在同步的可能
ExecutorService threadPool = Executors.newSingleThreadExecutor();

常用的API

  • execute(Runnable command ) 线程调用线程池资源的核心
  • Thread.CurrentThread()获得当前线程对象
  • shutdownNow()
  • shutdown();
    例子
ExecutorService threadPool = Executors.newSingleThreadExecutor();
       for (int i = 0; i < 10; i++) {
           final int task = i;
           threadPool.execute(new Runnable() {
               @Override
               public void run() { }
               }
           });
       }
       System.out.println("shutdown");
       threadPool.shutdownNow();
2.1 从任务中的返回值
  • Runnable是执行工作的独立任务,但是它不返回值,如希望在任务完成时能返回一个值,那么可以实现Callable接口而不是Runnable接口。Callable是一种具有类型参数的泛型,它的类型参数表示的是从call()而不是run()中返回的值,并且使用ExecutorService.submit()方法调用它
class TaskWithResult implements Callable<String>{
	private int id;
	public TaskWithResult(int id){
		this.id = id;
	}
	public String call(){
		return "result of TaskWithResult"+id;
	}
}
public class CallableDemo{
	public static void main(String[] args){
		ExecutorService exec = Executors.newCacheThreadPool();
		List<Future<String>> result  = new ArrayList<>();
		for(int i  = 0;i<10;i++){
			result.add(exec.submit(new TaskWithResult(1)));
		}
		for(Future<String> fs : result){
			try{
				System.out.println(fs.get());
			}catch(Exception e){
				e.printStackTrace();
			}finally{
				exec.shutdown();
			}
		}
	}
}
  • submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化,可以用isDone()查询Future是否完成,若完成,那么就有一个结果,通过get()获取,若未完成,直接用get()方法获取,则会阻塞,直至结果就绪。 一般先用isDone()判断是否完成,再用get。

二、共享受限资源

1、同步技术

  • 解决共享资源的竞争
    • 在《java编程思想》中是这样描述这个问题的,在就餐时,当叉子快要碰到最后一片食物时,突然食物消失了。是因为另外一个去食用了这片食物。这就是并发,所以在编程过程中,要避免出现这种情况
    • 解决方法:防止这种冲突的方法就是当资源被一个任务使用的时候,在其上加锁,第一个访问某项资源的任务必须锁定这项资源,使其他任务在被解锁之前,就无法访问它。

    基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,意思是在给定时间内,只允许一个任务访问共享资源。通常做法是在代码前面加上一条锁语句,使得在一段时间内,只有一个任务可以运行这段代码,因为锁的语句产生了一种相互排斥的效果,所以这样机制常常称为互斥量(mutex)


方案1、synchronized关键字
  • java提供了关键字synchronized的形式,防止资源冲突提供了内置支持,当任务要执行synchronized关键字保护的代码片段的时候,它将检验锁是否可用,然后获取锁,执行代码,释放锁。
  • 注意:
    1、要控制对共享资源的访问,得先把它保证进一个对象,然后把所有要访问这个资源的方法标记为synchronized。
    2、 对类的数据成员,都声明为private,而且只能通过方法来访问这些数据。然后对方法进行synchronized标记,这样就可以实现对数据访问的同步
    3、对于某个特定的对象来说,其所有synchronized方法共享同一把锁,这可以被用来防止多个任务同时访问被编码为对象内存
1.1 临界区
  • 有时,只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法.通过这样子方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。在这种方式被称为同步代码块

–示例

static class Outputer{
//代码内加锁
	 public void output(String name) {
            int len = name.length();
            //同步锁,必须是同一个对象,对有并发问题的代码进行同步
            //同步代码块
            synchronized (this) {
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println();
            }
        }
  //方法上加锁,默认检测的是this,就是该类的对象。
   public synchronized void output2(String name) {
            int len = name.length();
            for (int i = 0; i < len; i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();
        }
    // 检测的是该类的字节码对象,如果要和output同步,把this改为Outputer.class
    public static synchronized void output3(String name) {
        int len = name.length();
        for (int i = 0; i < len; i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();
    }
}
方案2、Lock对象
  • 在java.util.concurrent类库中还包含有定义在java.util.concurrent.locks中的显示互斥机制。Lock对象必须显式的创建,锁定和释放。和内建的锁相比,代码缺乏优雅性,但是对于解决某些问题,更加灵活。
  • 关键API
//创建锁对象
Lock lock = new ReentrantLock();
lock.lock();//上锁
try{
	...同步代码...
}finally{
	lock.unlock();//释放锁
}

注意(重点):锁是必须释放的,意味着,需要由finally关键字,在finally代码块中释放锁。不然一旦程序有异常,那么锁资源得不到释放,所有需要该锁的线程都不能执行

方案3、原子操作

在前面的两种方案中,都是通过锁进行了同步,但是出现同步的原因还有可能是原子问题。

比如在jvm中对long和double变量(64位)的读取和写入操作,是当成2个32位操作进行的。这样对long和double型变量的操作就不是原子性的,可能会进行上下文的切换,导致任务结果的不正确。

  • 解决方案:在定义long和double变量时,如果使用volatile关键字。就会获得原子性。
  • 这就是原子操作可以由线程机制来保证其不可中断。
3.1原子类

java SE5 引入了AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类,主要是用于性能调优,去掉同步锁

public class AtomicityTest implements Runnable {
//    private int i = 0;
    private AtomicInteger i = new AtomicInteger(0);
    public int getValue(){
        //return i;
        return  i.get();
    }
    private /*synchronized*/ void evenIncrement(){
        /*i++;
        * i++;
        * */
       i.addAndGet(2);
    }
    @Override
    public void run() {
        while (true){
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Aborting");
                System.exit(0);
            }
        },5000);
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while (true){
            int val = at.getValue();
            if(val%2!=0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

方案4、线程本地存储
  • 防止任务在共享资源上产生冲突的第二种方法就是根除对变量的共享。
  • 线程本地存储是一种自动化机制。可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有五个线程都要使用变量X表示的对象。那线程本地存储就会生成5个用于X的不同存储块,每个线程的数据不共享。
  • 重要API
    • get();访问对象内容
    • set();设置ThreadLocal的变量
// 单例设计模式
class MyThreadScopeData{
	private MyThreadScopeData(){}
	
	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal();
	
	public static MyThreadScopeData getInstance(){
		MyThreadScopeData instance = map.get();
        if (instance == null) {
            instance = new MyThreadScopeData();
            map.set(instance);
        }
        return instance;
	}
	
	private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class ThreadScopeShareData {
    private static ThreadLocal<Integer> x = new ThreadLocal<>();
    private static ThreadLocal<MyThreadScopeData> myThreadscopeData = new ThreadLocal<>();

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int data = new Random().nextInt();
                    System.out.println(Thread.currentThread().getName() + "has put data :" + data);
                    x.set(data);
                    MyThreadScopeData.getInstance().setName("name"+data);
                    MyThreadScopeData.getInstance().setAge(data);
                    new A().get();
                    new B().get();
                }
            }).start();
        }
    }

     static class A {
        public void get() {
            int data = x.get();
            System.out.println("A from " + Thread.currentThread().getName() + " has put data :" + data);
            MyThreadScopeData mydata = MyThreadScopeData.getInstance();
            System.out.println("A from mydata = " + mydata.getName() + " my age " + mydata.getAge()*10);
        }
    }

    static class B {
        public void get() {
            int data = x.get();
            System.out.println("B from" + Thread.currentThread().getName() + "has put data :" + data);
            MyThreadScopeData mydata = MyThreadScopeData.getInstance();
            System.out.println("A from mydata = " + mydata.getName() + " my age " + mydata.getAge()/10);
        }
    }
}

在上述示例中,主线程,AB线程各种独立,尽管都是从一个数据源获取数据,但是可以自己对数据进行操作,而不造成相互影响。

-------------------------------10.10 11:57-----------------------------------分割线-------------------------


三、线程中断

3.1 线程状态

  • 新建(new):当线程被创建时,只有短暂时间会处于该状态,此时它已经分配了必需的系统资源,并执行了初始化。此刻该线程已经有资格获得cpu时间了,之后调度器把这个线程转变为可运行状态或阻塞状态。
  • 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态
  • 阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何cpu时间,直到线程重新进入就绪状态,它才可能执行操作。
  • 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到cpu时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
    在这里插入图片描述
进入阻塞状态的原因
  1. 通过sleep使任务进入休眠状态,在这种情况下,任务在指定时间内不会运行。
  2. 通过wait()使线程挂起。直到线程得到了notify()和notifyAll()消息(或者再javaSE5的java.util.concurrent类库中等价的signal()或signAll()消息),线程才会进入就绪状态,
  3. 任务在等待某个输入和输出
  4. 任务试图在某个对象上调用其某个同步控制方法,但是对象锁不可用,因为另一个任务已经获取了该锁
中断

有时候,对于处于阻塞状态的任务,需要对其进行终止,那么就需要强制这个任务跳出阻塞状态。

  • Thread类包含interrupt()方法
  • Executor上调用shutdownNow()方法

线程通信

前面学习的是,如何使线程互不干扰,通过使用锁技术(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另外一个任务的资源。
下一步学习,多线程之间如何相互合作,以使得多个任务可以一起工作去解决某个问题。

  • 任务协作时,关键问题是这些任务之间的握手,为了实现这种握手,我们使用了相同的基础特性:互斥。

1. wait()和notifyAll()

猜你喜欢

转载自blog.csdn.net/weixin_41263632/article/details/82977097