第十五章、多线程

多线程

1. 进程与线程初识

1.1 进程

  • 进程是程序的一次动态执行过程, 占用特定的地址空间。

  • 每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。

  • 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。

  • 进程的查看:

    • Windows系统: Ctrl+Alt+Del,启动任务管理器即可查看所有进程。

    • Unix系统: ps or top。

1.2 线程

  • 一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。

  • 一个进程可拥有多个并行的(concurrent)线程。

  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。

  • 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。

  • 线程的启动、中断、消亡,消耗的资源非常少。

1.3 线程和进程的区别

  • 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。
  • 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
  • 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。
  • 多进程: 在操作系统中能同时运行多个任务(程序)。
  • 多线程: 在同一应用程序中有多个顺序流同时执行。
  • 线程是进程的一部分,所以线程有的时候被称为轻量级进程。
  • 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
  • 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。

2. 多线程实现方案

2.1 继承Thread类实现

  1. 实现步骤

    • 自定义类线程类(例:MyThread),继承Thread类

    • MyThread重写run方法

    • 创建线程对象

    • 启动线程

  2. 线程方法

    run()				线程执行和普通方法一致
    start()				开启线程
    setName()			设置当前线程名
    getName()			获取当前线程名
    currentThread() 	获取当前主线程
    
  3. 代码示例

    public class TestThread extends Thread {//自定义类继承Thread类
        //run()方法里是线程体
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
            }
        }
     
        public static void main(String[] args) {
            TestThread thread1 = new TestThread();//创建线程对象
            thread1.setName("线程1")//设置线程名称 
            thread1.start();//启动线程
            TestThread thread2 = new TestThread();
            thread2.setName("线程2")
            thread2.start();
        }
    }
    

2.2 Runnable接口实现

  1. 实现步骤

    • 自定义类线程类(例:MyRunnable),实现Runnable接口

    • MyRunnable重写run方法

    • 创建线程对象

    • 启动线程

  2. 线程方法:和继承Thread类实现多线程一致

  3. 代码示例

    public class TestThread2 implements Runnable {//自定义类实现Runnable接口;
        //run()方法里是线程体;
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
        public static void main(String[] args) {
            //创建线程对象,把实现了Runnable接口的对象作为参数传入;
            Thread thread1 = new Thread(new TestThread2());
            thread1.setName("线程1")//设置线程名称 
            thread1.start();//启动线程;
            Thread thread2 = new Thread(new TestThread2());
            thread2.setName("线程1")//设置线程名称 
            thread2.start();
        }
    }
    

3. 线程调度和线程控制

  • 线程休眠 / 睡眠
    • public static void sleep(long millis)
  • 线程加入 / join线程(放在start()方法后调用)
    • public final void join() 等待线程终止,才继续执行下一个线程
  • 线程礼让 线程让步
    • public static void yield() 放在线程体中操作
    • 暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。让多个线程的执行更和谐,但是不能靠他保证一人一次。
  • 后台线程 / 守护线程
    • public final void setDaemon(boolean on)
    • 将指定线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
  • 中断线程
    • public final void stop() 让线程停止,过时了,但是还可以使用。
    • public void interrupt() 中断线程。 把线程的状态终止,并抛出一个InterruptedException。

4. 线程生命周期

图11-4 线程生命周期图.png

5. 线程同步

  • 概念:处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

  • synchronized 方法实现

    • 代码实现

      package demo6;
      
      public class MyCinema implements Runnable {
      
      	static int poll = 100;
      	Object obj = new Object();
      
      	@Override
      	public void run() {
      		while (true) {
      			// synchronized (obj) {
      			// synchronized (this) { 等同于 同步方法
      			// synchronized (MyCinema.class) { 等同有静态同步方法
      			show();
      			// }
      		}
      
      	}
      
      	// 同步方法就是把同步方法关键字加到方法上
      	// 同步方法锁对象是谁 锁对下那个就是this
      	// 静态方法所对象是谁 MyCinema.class
      	public synchronized void show() {
      		if (poll > 0) {
      			try {
      				Thread.sleep(100);
      			} catch (InterruptedException e) {
      				// TODO Auto-generated catch block
      				e.printStackTrace();
      			}
      			System.out.println(Thread.currentThread().getName() + "正在出售第" + (poll--) + "张票。");
      		}
      
      	}
      
      }
      
      package demo6;
      
      /*
       * 需求:
       * 		某电影院目前正在上映喜剧大片,共有100张票,而它有3个售票窗口售票,设计一个程序模拟该电影院售票
       */
      public class TestMyCinema {
      	public static void main(String[] args) {
      
      		MyCinema my = new MyCinema();
      
      		Thread t1 = new Thread(my, "张三");
      		Thread t2 = new Thread(my, "李四");
      		Thread t3 = new Thread(my, "王五");
      
      		t1.start();
      		t2.start();
      		t3.start();
      
      	}
      }
      
  • Lock类实现

    • 代码实现

      package demo7;
      
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      /*
       * 虽然我们可以理解同步代码块合同部方法的锁对象问题,但是我们并没有直接看到在哪里加锁
       * 在哪里释放锁,为了更加清晰的表达如何加锁和释放锁,JDK5以后的版本提供了一个新的锁对象Lock
       * 		Lock  接口
       * 			void lock()    加锁
       * 			void unlock()  释放锁
       * 		ReentrantLock   实现类
       */
      
      public class MyLock implements Runnable {
      
      	static int poll = 100;
      	Object obj = new Object();
      
      	// 声明lock锁
      	Lock lock = new ReentrantLock();
      
      	@Override
      	public void run() {
      		// TODO Auto-generated method stub
      		while (true) {
      			// synchronized (obj) {
      			// 加锁
      			lock.lock();
      			if (poll > 0) {
      				try {
      					Thread.sleep(100);
      				} catch (InterruptedException e) {
      					e.printStackTrace();
      				}
      				System.out.println(Thread.currentThread().getName() + "正在出售第" + (poll--) + "张票。");
      			}
      			// 释放锁
      			lock.unlock();
      			// }
      		}
      	}
      }
      
      package demo7;
      
      public class TestMyLock {
      	public static void main(String[] args) {
      
      		MyLock my = new MyLock();
      
      		Thread t1 = new Thread(my, "张三");
      		Thread t2 = new Thread(my, "李四");
      		Thread t3 = new Thread(my, "王五");
      
      		t1.start();
      		t2.start();
      		t3.start();
      
      	}
      }
      

6. 死锁

  • 概念: 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

  • 代码实现

    package demo8;
    
    public class DeadLock extends Thread {
    
    	private boolean flg;
    
    	public DeadLock(boolean flg) {
    		this.flg = flg;
    	}
    
    	@Override
    	public void run() {
    		// flg = true 中国人;flg = false 美国人
    		if (flg) {
    			synchronized (LockObject.obj1) {
    				System.out.println("一根筷子");
    				synchronized (LockObject.obj2) {
    					System.out.println("一把叉子");
    				}
    			}
    		} else {
    			synchronized (LockObject.obj2) {
    				System.out.println("一把刀");
    				synchronized (LockObject.obj1) {
    					System.out.println("一根筷子");
    				}
    			}
    		}
    	}
    }
    
    package demo8;
    
    public class LockObject {
    	static Object obj1 = new Object();
    	static Object obj2 = new Object();
    
    }
    
    package demo8;
    
    /*
     * 死锁:
     * 		美国人吃饭用一把叉子和一把刀子
     * 		中国人吃饭用一双筷子
     * 	
     * 		现在的情况是:	
     * 			美国人手里一把叉子和一根筷子
     * 			中国人手里一根筷子和一把刀子
     * 
     *		两者相互等待对方将东西交出来,却没有人主动交,所以陷入死锁
     */
    public class TestLock {
    	public static void main(String[] args) {
    
    		DeadLock d1 = new DeadLock(true); // 中国人
    		DeadLock d2 = new DeadLock(false); // 美国人
    
    		Thread t1 = new Thread(d1, "锁1");
    		Thread t2 = new Thread(d2, "锁2");
    
    		t1.start();
    		t2.start();
    
    		/*
    		 * DeadLock d1 = new DeadLock(true); // 中国人 DeadLock d2 = new
    		 * DeadLock(false); // 美国人
    		 * 
    		 * d1.start(); d2.start();
    		 */
    	}
    }
    

7. 线程通信(生产者消费者模型)

7.1 相关概念

  • 生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

  • 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。

  • 缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

图11-17 生产者消费者示意图.png

7.2 相关方法

表11-2 线程通信常用方法.png

  • 以上方法均是java.lang.Object类的方法,这些方法只能在同步方法或同步代码中使用,否则抛异常。

7.3 代码实现

// 生产对象
package demo9;

public class Book {

	private String name;
	private int price;

	// 如果为true说明仓库里面有书,如果为false说明仓库里面没有书
	// 如果有书,就通知消费者去消费
	// 如果没有书,就通知生产者取生产
	public boolean flag;

	public String getName() {
		return name;
	}

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

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

}
// 消费者
package demo9;

public class Consumption implements Runnable {

	private Book book;

	public Consumption(Book book) {
		this.book = book;
	}

	@Override
	public void run() {

		// 为了模拟一直消费
		while (true) {
			synchronized (book) {
				if (!book.flag) {
					try {
						book.wait(); // 等待,如果生产者那边有书就继续往下走
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				} else {
					System.out.println(
							Thread.currentThread().getName() + "买了" + book.getName() + "价格是" + book.getPrice());
					book.flag = false;
					book.notify();
				}
			}
		}
	}
}
// 生产者
package demo9;

public class Production implements Runnable {

	private Book book;

	int num = 0;

	public Production(Book book) {
		this.book = book;
	}

	@Override
	public void run() {
		// 为了模拟一直生产
		while (true) {
			synchronized (book) {
				if (book.flag) {
					try {
						book.wait(); // 如果有书,生产者线程就进入等待,不生产了
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (num % 2 == 0) {
					book.setName("不能承受生命之轻");
					book.setPrice(123);
					System.out.println(
							Thread.currentThread().getName() + "生产了" + book.getName() + "价格是" + book.getPrice());
				} else {
					book.setName("作为表象与意志的世界");
					book.setPrice(99);
					System.out.println(
							Thread.currentThread().getName() + "生产了" + book.getName() + "价格是" + book.getPrice());
				}
				num++;
				book.flag = true;
				book.notify(); // 唤醒消费线程

			}
		}

	}

}
// 实现类
package demo9;

/*
 * 生产者:
 * 		先看是否有数据,有就等待,没有就生产,生产完成后就通知消费者过来消费
 * 消费者:
 * 		先看是否有数据,悠久消费,没有就等待,通知生产者过来生产
 * 
 * 		
 * 		等待唤醒
 * 			wait()		等待
 * 			notify()    唤醒单个线程
 * 			ntifyAll()	唤醒所有等待的线程
 * 		
 * 		wait和sleep的区别?
 * 			wait等待需要被唤醒
 * 			sleep休眠,一定时间会自动苏醒
 */
public class TestThread {
	public static void main(String[] args) {

		Book b = new Book();

		Consumption c = new Consumption(b);
		Production p = new Production(b);

		Thread t1 = new Thread(c, "消费者"); // 消费者
		Thread t2 = new Thread(p, "生产者"); // 生产者

		t1.start();
		t2.start();

	}
}

8.线程组

  • 概念:对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

  • 代码实现

    package demo10;
    
    public class MyRunnable implements Runnable {
    
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			try {
    				Thread.sleep(100);
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName() + "  " + i);
    		}
    	}
    }
    
    package demo10;
    
    /*
     * 	线程组,把多个线程组合到一起
     * 	他可以对一批线程进行分类管理,Java允许程序直接对线程进行控制
     */
    public class ThreadGroupDemo {
    	public static void main(String[] args) {
    		// 我们如何修改线程所在的线程组呢?
    		// 创建一个线程组
    		// 创建其他线程的时候,把其他线程的组制定为我们新建的线程组
    		// ThreadGroup(String name) 构造一个新线程组
    		ThreadGroup tg = new ThreadGroup("这是一个新线程组");
    
    		MyRunnable my = new MyRunnable();
    
    		// 如果想指定下列线程属于某个组,怎么办?
    		Thread t1 = new Thread(tg, my, "线程1");
    		Thread t2 = new Thread(tg, my, "线程2");
    
    		// 线程类中方法:getThreadGroup() 返回该线程所属的线程组
    		ThreadGroup threadGroup1 = t1.getThreadGroup();
    		ThreadGroup threadGroup2 = t2.getThreadGroup();
    
    		// 线程组方法:getName()
    		String name1 = threadGroup1.getName();
    		String name2 = threadGroup2.getName();
    
    		// 通过结果我们知道,线程默认情况下属于main线程组
    		System.out.println(name1);
    		System.out.println(name2);
    
    		tg.setDaemon(true); // 通过组名设置后台线程,表示该线程组的线程都是后台线程
    	}
    }
    

9. 线程池

9.1 线程池的优势

  • 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  • 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  • 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))
  • 提供更强大的功能,延时定时线程池

9.2 线程池流程

img

  • 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则
  • 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
  • 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

9.3 Executors实现线程池

方法名 功能
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExecutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
  • 具体方法

    • submit() 创建线程对象
    • shutdown() 结束线程池
  • 代码实现

    package demo11;
    
    public class MyRunnable implements Runnable {
    
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			System.out.println(Thread.currentThread().getName() + "  " + i);
    
    		}
    	}
    
    }
    
    package demo11;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /*
     * 	线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次返回到线程池中成为空闲状态,等待下一个对象来使用
     * 
     * 		如何实现线程池代码?
     * 			1.创建线程池对象,控制要创建几个线程对象
     * 				public static ExecutorsService new FixedThreadPool(int nThreads)
     * 			2.线程池的线程可以执行
     * 				可以执行Runnable对象或者Callable对象代表的线程
     * 			3.调用方法如下:
     * 				Future<?> submit(Runnable task)
     * 				<T> Future submit((Callable)<T> task)
     */
    public class ExecutorsDemo {
    	public static void main(String[] args) {
    
    		// 创建线程池对象,控制要创建几个线程对象
    		ExecutorService pool = Executors.newFixedThreadPool(2);
    
    		pool.submit(new MyRunnable());
    		pool.submit(new MyRunnable());
    		pool.submit(new MyRunnable());
    
    		// shutdown() 启动一次,顺序关闭,执行以前提交的任务,但不能接收新的任务
    		// 结束线程
    		pool.shutdown();
    	}
    }
    

10. 定时器

  • java.util.Timer

    • 概念:Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。
  • java.util.TimerTask

    • 概念:TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
  • 具体方法

    • schedule() 添加计时器
    • cancel() 结束计时器
  • 代码实现

    package demo12;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    /*
     * 定时器:可以让我们在指定的时间内做某件事情,还可以重复地做某件事情
     * 
     * 依赖timer和TImerTask这里两个类
     * 		timer:定时
     * 			public Timer()
     * 			public void schedule(TimerTask task,long delay) 在指定延迟后执行指定任务
     * 			public void schedule(TimerTask task,long delay,long period)  安排指定的任务(task)从指定的延迟后(delay)开始进行重复与固定的延时时间	(period)	
     *			cancel()  终止此计时器,丢弃所有当前已安排的任务
     *
     *		TimerTask:任务
     */
    public class TimerDemo1 {
    	public static void main(String[] args) {
    		// 创建定时器对象
    		Timer t = new Timer();
    
    		// 三秒后上厕所
    		t.schedule(new MyTask(), 3000);
    
    		// 结束任务
    		t.schedule(new MyTask1(t), 3000);
            
            // 设置循环执行时间
            t.schedule(new MyTask2(), 3000, 2000);
    
    	}
    }
    
    class MyTask1 extends TimerTask {
    
    	private Timer t;
    
    	public MyTask1(Timer t) {
    		this.t = t;
    	}
    
    	@Override
    	public void run() {
    		System.out.println("上厕所中....");
    		t.cancel();
    	}
    
    }
    
原文地址:https://www.cnblogs.com/borntodie/p/14141114.html