进程与线程
进程的概念: 线程依赖于进程,进程是系统进行资源分配和调用的独立单位,每个进程都有他自己的内存空间和系统资源。单进程计算机同一时间点只能做一件事情。多核CPU支持多进程,同一时间可以执行多个任务。
线程的概念: 同一进程内又可以执行多个任务,每一个任务可以称之为一个线程。线程,是程序执行单位,执行路径。是程序使用CPU的基本单位。单线程,程序只有一条执行路径。多线程,程序有多条执行路径。
进程与线程的区别: 打开游戏是一个进程,打开音乐播放器是一个进程。点击播放是一个线程,下载音乐是一个线程。
多线程的意义: 多线程的存在,不是提高程序的执行速度,而是为了提高应用程序的使用率。程序的执行本质上都是在抢占CPU的资源,CPU的执行权。当多个进程抢占CPU资源时,哪个进程如果执行路径较多,则会有更高的几率抢到CPU的执行权,所以线程的执行具有随机性。
并行与并发的区别: 并行是逻辑上同时发生,指在某一时间段内同时运行多个程序。并发是物理上同时发生,指在某一时间点同时运行多个程序。
一、多线程程序的实现方式
线程是依赖进程存在的,要想实现多线程就应该先创建出来一个进程。而进程是由系统创建的,所以要通过调用系统功能创建一个进程。Java不能直接调用系统功能,所以没有办法直接实现多线程程序。所以Java通过调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用,可以得到一些类供我们使用,就可以实现多线程程序了。
(一) 实现方式1
- 创建Thread类的子类
- 重写Thread类中的run();方法(run();方法中封装的是必须被线程执行的代码,run方法中一般书写比较耗时的代码)
- start();开启线程
(二) 实现方式2
实现Runnable接口,优点是扩展性强,在实现一个接口的同时,还能再去继承其他类。
- 定义一个类并实现Runnable接口
- 重写接口中的run()方法
- 创建线程对象,把Runnable接口中子类对象,作为参数传递进来
- 开启线程
public class MyThread implements Runnable {
@Override
public void run() {
for(int x = 0 ; x < 100 ; x++){
System.out.println(Thread.currentThread().getName() + "---" + x);
}
}
}
--------------------------------------------------------------------------------------------------------
public static void main(String[] args) {
// 创建对象
MyThread mt1 = new MyThread() ;
MyThread mt2 = new MyThread() ;
// 创建Thread对象
Thread t1 = new Thread(mt1 , "张三") ;
Thread t2 = new Thread(mt2 , "李四") ;
// setName给线程设置名称
// t1.setName("张三") ;
// t2.setName("李四") ;
// 启动
t1.start() ;
t2.start() ;
}
二、多线程的数据安全问题
(一) 线程安全问题出现的条件
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
(二) 同步代码块的方式解决线程安全问题
synchronized(对象){//不能在括号了直接new 对象 new 了 就没效果,要定义为静态成员变量,才能被所有线程共享
要被同步的代码 ;
}
- 同步的好处:同步的出现解决了多线程的安全问题。
- 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
- 同步代码块的锁对象:任意一个对象
- 同步方法的锁对象:是this
- 静态同步方法的锁对象:就是当前类对应的字节码文件对象
(三) Lock锁解决线程安全问题
由于同步代码块不能直观的看到在哪里加了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK1.5以后提供了新的锁对象Lock
void lock();//开锁
void unlock();//关锁
---------------------------------------------------------------
public class MyThread implements Runnable {
// 定义票数
private static int tickets = 100 ;
// 创建Lock锁对象
private static final Lock lock = new ReentrantLock() ;
@Override
public void run() {
while(true){
// 添加锁
lock.lock() ;
if(tickets > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售" + (tickets--) + "张票");
}
// 释放锁
lock.unlock() ;
}
}
}
--------------------------------------------------------
public class MyThreadDemo {
public static void main(String[] args) {
// 创建MyThread类的对象
MyThread mt = new MyThread() ;
// 创建3个线程对象
Thread t1 = new Thread(mt , "窗口1") ;
Thread t2 = new Thread(mt , "窗口2") ;
Thread t3 = new Thread(mt , "窗口3") ;
// 启动线程
t1.start() ;
t2.start() ;
t3.start() ;
}
}
(四) 死锁现象
同步嵌套,将导致死锁。
public interface MyLock {
public static final Object objA = new Object() ;
public static final Object objB = new Object() ;
}
----------------------------------
public class MyThread extends Thread {
private boolean flag ;
public MyThread(boolean flag){
this.flag = flag ;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.objA) {
System.out.println("true......objA...................");
//嵌套同步代码块
synchronized (MyLock.objB) {
System.out.println("true.......objB.............");
}
}
}else {
synchronized (MyLock.objB) {
System.out.println("false......objB...................");
//嵌套同步代码块
synchronized (MyLock.objA) {
System.out.println("false.......objA.............");
}
}
}
}
}
-----------------------------------------
public class DeadLockDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread mt1 = new MyThread(true) ;
MyThread mt2 = new MyThread(false) ;
// 启动线程
mt1.start() ;
mt2.start() ;
}
}
(五) 等待唤醒机制
线程的等待唤醒:Object类
void wait ();
在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
void notify();
唤醒在此对象监视器上的等待的单个线程。
void notifyAll();
唤醒在此对象监视器上等待的所有线程。
生产线程:如果没有资源资源我就生产,有了资源我就等待,通知消费线程来消费
消费线程:有了资源我就消费,没有资源我就等着,你通知生产线程生产
sleep()和wait()方法:
相同点:这两个方法都能使线程处于阻塞状态。
区别:sleep()方法必须要一个时间,wait()方法可以要时间也可以不传时间。sleep()线程休眠后不释放锁,wait()一旦等待就要释放锁。
(六)线程的生命周期
三、线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而线程池作为一个装有线程对象的容器,可以帮我们管理线程对象。线程池会预先创建一些线程对象,放在线程池内,当有任务需要执行时,就可以让线程池中的线程去执行任务,当任务完成后,线程可回收。
利用Executors工厂来产生线程池:
public static ExecutorService newCachedThreadPool(): //根据任务的数量来创建线程对应的线程个数(无边界线程池)
public static ExecutorService newFixedThreadPool(int nThreads): //固定初始化几个线程(固定大小线程池)
public static ExecutorService newSingleThreadExecutor(): //初始化一个线程的线程池(单线程线程池)
ExecutorService的提交与关闭:
<T> Future<T> submit(Callable<T> task); //提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
Future<?> submit(Runnable task);//提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
void shutdown();//启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
Callable有返回值,Future中有一个get()方法可以获取返回的结果。
四、定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。开发中使用Quartz开源调度框架。
Timer:
public Timer(); //创建一个新的定时器
public void schedule(TimerTask task, long delay): //在指定毫秒值后执行定时任务
public void schedule(TimerTask task,long delay,long period); //在指定毫秒值delay后第一次执行任务,period毫秒后重复执行任务
public void schedule(TimerTask task, Date time): //在指定日期后执行定时任务
public void schedule(TimerTask task, Date firstTime, long period): //在指定日期后执行定时任务,相隔period毫秒后重复执行该任务
public void cancel(); //终止此定时器,丢弃当前已安排任务
TimeTask:
public abstract void run(); //此计时器要执行的任务
public boolean cancel(); //取消此定时器任务
五、内存可见性的问题
(一) Java内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,
线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。
线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
(二) Java中的可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的, 当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
(三) volatile关键字
Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
volatile不具有互斥性,但不能保证变量的原子性操作。JDK1.5之后可通过 java.util.concurrent.atomic包下的类来保证原子性操作。
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
- AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
- AtomicIntegerArray 、 AtomicLongArray
- AtomicMarkableReference
- AtomicReferenceArray