并发与多线程
并发与多线程
基本概念
并发与并行
- 并发:指两个或多个事件在同一时间间隔内发生 。当有多个线程在操作时,如果系统只有一个cpu,则它根本不可能真正同时进行一个以上的线程,它只能把cpu运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式称之为并发(concurrent)
- 并行:指两个或者多个事件在同一时刻发生 。当系统有一个以上cpu时,则线程的操作有可能非并发。当一个cpu执行一个线程时,另一个cpu可以执行另一个线程,两个线程互不抢占cpu资源,可以同时进行,这种方式称之为并行(parallel)
进程与线程
- 一个程序可能有多个进程,一个进程由多个线程和共享资源组成
- 进程:拥有资源的基本单位
- 线程:独立调度分派的基本单位
线程
创建线程
thread
-
继承 thread 类(thread 实现了 runnable 接口)
-
重写 run 方法
-
start( ) 方法启动线程
runnable
- 实现 runnable 接口
- 重写 run 方法
- new thread(runnable target),new thread(runnable target,string name)
多个 thread 实例共用一个 runnable,这些线程的 run 方法相同,可以共享相同的数据
但是存在线程同步问题
public class runnabletest implements runnable { private int ticket = 10; public void run() { while (true) { if (ticket > 0) { system.out.println(thread.currentthread().getname() + "售出" + ticket + "号票"); ticket--; } else system.exit(0); } } public static void main(string[] args) { runnabletest rt = new runnabletest(); thread t1 = new thread(rt, "1号窗口"); thread t2 = new thread(rt, "2号窗口"); t1.start(); t2.start(); } }
1号窗口售出10号票 1号窗口售出9号票 1号窗口售出8号票 1号窗口售出7号票 2号窗口售出7号票 2号窗口售出5号票 1号窗口售出6号票 2号窗口售出4号票 1号窗口售出3号票 2号窗口售出2号票 1号窗口售出1号票
匿名类
匿名类可以方便的访问方法的局部变量,但是必须声明为 final,因为匿名类和普通局部变量生命周期不一致
jdk7 中已不再需要显示声明为 final,实际上被虚拟机自动隐式声明了
public static void main(string[] args) { new thread() { public void run() { //内容 } }.start(); new thread(new runnable() { public void run() { //内容 } }).start(); }
callable
-
创建 callable 的实现类,并冲写 call( ) 方法,该方法为线程执行体,并且该方法有返回值
-
创建 callable 实现类的实例,并用 futuertask 类来包装 callable 对象,该 futuertask 封装了 callable 对象 call( ) 方法的返回值
-
实例化 futuertask 类,参数为 futuertask 接口实现类的对象来启动线程
-
通过 futuertask 类的对象的 get( ) 方法来获取线程结束后的返回值
public class callabletest implements callable<integer> { //重写执行体 call() public integer call() throws exception { int i = 0; for (; i < 10; i++) { // } return i; } public static void main(string[] args) { callable call = new callabletest(); futuretask<integer> f = new futuretask<integer>(call); thread t = new thread(f); t.start(); //得到返回值 try { system.out.println("返回值:" + f.get()); } catch (exception e) { e.printstacktrace(); } } }
print
返回值:10
线程方法
-
线程执行体:run( )
-
启动线程:start( )
-
thread 类方法
方法 描述 public final void setname(string name) 改变线程名称 public final void setpriority(int priority) 设置优先级 public final void setdaemon(boolean on) 设为守护线程,当只剩下守护线程时自动结束 public final boolean isalive( ) 测试线程是否处于活动状态 public static void yield( ) 暂停当前线程(回到就绪状态) public static void sleep(long millisec) 进入休眠状态 public final void join( ) 暂停当前线程,等待调用该方法线程执行完毕 public final void join(long millisec) 暂停当前线程指定时间 public static thread currentthread() 返回对当前正在执行的线程对象的引用
线程状态
-
就绪状态:
- start( ) 方法进入就绪状态,等待虚拟机调度
- 运行状态调用 yield 方法会进入就绪状态
- lock 池中的线程获得锁后进入就绪状态
-
运行状态:就绪状态经过线程调度进去运行状态
-
阻塞状态:
- 休眠:调用 sleep 方法
- 对象 wait 池:调用 wait 或 join 方法,被 notify 后进入 lock 池
- 对象 lock 池:未获得锁
-
死亡状态:run 方法执行完毕
graph tb t(新线程)--start方法-->a(就绪状态) a--线程调度-->b(运行状态) b--yield方法-->a b--sleep方法-->d(阻塞:休眠) b--wait或join方法-->e(阻塞:wait池) b--未获得锁-->f(阻塞:lock池) b--run方法执行完-->c(死亡状态) d--时间到-->a e--notify方法-->f f--获得锁-->a
线程同步
保证程序原子性、可见性、有序性的过程
阻塞同步
基于加锁争用的悲观并发策略
synchronized
-
synchronized 含义
-
使用 synchronized 可以锁住某一对象, 当其他线程也想锁住该对象以执行某段代码时,必须等待已经持有锁的线程释放锁
-
释放锁的方式有互斥代码执行完毕、抛出异常、锁对象调用 wait 方法
-
-
不同的使用方式代表不同的锁粒度
- 修饰普通方法 = synchronized(this)
- 修饰静态方法 = synchronized(x.class)
- 修饰代码块(对象 extends object)
reentrantlock
-
创建 lock 锁
reentrantlock 实现了 lock 接口, lock lock = new reentrantlock( )
-
lock 含义
-
使用 lock( ) 方法表示当前线程占有 lock 对象
-
释放该对象要显示掉用 unlock( ) 方法 ,多在 finally 块中进行释放
-
-
trylock 方法
- synchronized 会一直等待锁,而 lock 提供了 trylock 方法,在指定时间内试图占用
- 使用 trylock, 释放锁时要判断,若占用失败,unlock 会抛出异常
-
lock 的线程交互
-
通过 lock 对象得到一个 condition 对象,condition condition = lock.newcondition( )
-
调用这个condition对象的:await,signal,signalall 方法
-
-
示例
public class locktest { public static void log(string msg)//日志方法 { simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss"); date date = new date(); string datestr = sdf.format(date); system.out.println(datestr + " " + thread.currentthread().getname() + " " + msg); } public static void main(string[] args) { lock lock = new reentrantlock(); new thread("t1") { public void run() { boolean flag = false; try { log("线程已启动"); log("尝试占有lock"); flag = lock.trylock(1, timeunit.seconds); if (flag) { log("成功占有lock"); log("执行3秒业务操作"); thread.sleep(3000); } else { log("经过1秒钟尝试,占有lock失败,放弃占有"); } } catch (interruptedexception e) { e.printstacktrace(); } finally { if (flag) { log("释放lock"); lock.unlock(); } } log("线程结束"); } }.start(); try { //先让 t1 先执行两秒 thread.sleep(2000); } catch (interruptedexception e1) { e1.printstacktrace(); } new thread("t2") { public void run() { boolean flag = false; try { log("线程启动"); log("尝试占有lock"); flag = lock.trylock(1, timeunit.seconds); if (flag) { log("成功占有lock"); log("执行3秒的业务操作"); thread.sleep(3000); } else { log("经过1秒钟的尝试,占有lock失败,放弃占有"); } } catch (interruptedexception e) { e.printstacktrace(); } finally { if (flag) { log("释放lock"); lock.unlock(); } } log("线程结束"); } }.start(); } }
print
2019-11-07 15:50:01 t1 线程已启动 2019-11-07 15:50:01 t1 尝试占有lock 2019-11-07 15:50:01 t1 成功占有lock 2019-11-07 15:50:01 t1 执行3秒业务操作 2019-11-07 15:50:03 t2 线程启动 2019-11-07 15:50:03 t2 尝试占有lock 2019-11-07 15:50:04 t2 经过1秒钟的尝试,占有lock失败,放弃占有 2019-11-07 15:50:04 t2 线程结束 2019-11-07 15:50:04 t1 释放lock 2019-11-07 15:50:04 t1 线程结束
-
synchronized 和 lock 区别
- synchronized 是关键字,lock 是接口, synchronized是内置的语言实现,lock是代码层面的实现
- synchronized 执行完毕自动释放锁,lock 需要显示 unlock( )
- synchronized 会一直等待,尝试占用锁,lock 可以使用 trylock,在一段时间内尝试占用,时间到占用失败则放弃
非阻塞同步
非阻塞同步是一种基于冲突检测和数据更新的乐观并发策略
actomic 类
-
原子操作
- 原子操作是不可中断的操作,必须一次性执行完成
- 赋值操作是原子操作,但 a++ 不是原子操作, 而是取值、加一、赋值三个步骤
- 一个线程取 i 的值后,还没来得及加一,第二个线程也来取值,就产生了线程安全问题
-
actomic 类的使用
- jdk6 以后,新增包 java.util.concurrent.atomic,里面有各种原子类,比如 atomicinteger
- atomicinteger 提供了各种自增,自减等方法,这些方法都是原子性的。换句话说,自增方法 incrementandget 是线程安全的
- 10000 个线程做 value 加一的操作,用 a++ 方式得出不准确的结果,用原子类 atomicinteger 的 addandget( ) 方法得出正确结果
public class threadtest { static int value1 = 0; static atomicinteger value2 = new atomicinteger(0);//原子整型类 public static void main(string[] args) { for (int i = 0; i < 100000; i++) { new thread() { public void run() { value1++; } }.start(); new thread() { public void run() { value2.addandget(1);//value++的原子操作 } }.start(); } while (thread.activecount() > 2) { thread.yield(); } system.out.println(value1); system.out.println(value2); } }
print
99996 100000
无同步方案
如果一个方法不涉及共享数据,那么他天生就是线程安全的
可重入代码
可以在代码执行的任何时刻中断它,转而去执行另外一段代码,在控制权返回之后,原来的程序不会出现任何的错误
-
一个方法返回结果是可以预测的,输入了相同的数据,就能返回相同的结果,那这个方法就具有可重入性,也就是线程安全的
-
栈封闭是一种可重用代码
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量保存在虚拟机栈中,属于线程的私有区域,所以不会出现线程安全性
public class threadtest { static void add() { int value = 0; for (int i = 0; i < 1000; i++) { value++; } system.out.println(value); } public static void main(string[] args) { executorservice threadpool = executors.newcachedthreadpool(); threadpool.execute(() -> add()); threadpool.execute(() -> add()); threadpool.shutdown(); } }
print
1000 1000
线程本地存储
-
把共享数据的可见范围限制在同一个线程之内,即便无同步也能做到避免数据争用
-
使用 java.lang.threadlocal 类来实现线程本地存储功能
- threadlocal 变量是一个不同线程可以拥有不同值的变量,所有的线程可以共享一个threadlocal对象
- 任意一个线程的 threadlocal 值发生变化,不会影响其他的线程
- 用set()和get()方法对threadlocal变量进行赋值和查看其值
public class threadlocaldemo { public static void main(string[] args) { threadlocal threadlocal1 = new threadlocal(); thread t1 = new thread(() -> { threadlocal1.set(1); try { thread.sleep(3000); } catch (interruptedexception e) { e.printstacktrace(); } system.out.println(threadlocal1.get()); }); thread t2 = new thread(() -> threadlocal1.set(2)); t1.start(); t2.start(); } }
print
1
-
threadlocal 原理
- 每个线程都有t一个 threadlocal.threadlocalmap 对象,调用 threadlocal1.set(t value) 方法时,将 threadloacl1 和 value 键值对存入 map
- threadlocalmap 底层数据结构可能导致内存泄露,尽可能在使用 threadlocal 后调用 remove( )方法
死锁
死锁条件
- 互斥条件
- 请求与保持条件
- 不可剥夺条件
- 循环等待条件(环路条件)
java死锁示例
public static void main(string[] args) { object o1 = new object(); object o2 = new object(); thread t1 = new thread() { public void run() { synchronized (o1)//占有 o1 { system.out.println("t1 已占有 o1"); try { thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o1 } catch (interruptedexception e) { e.printstacktrace(); } system.out.println("t1 试图占有 o2"); system.out.println("t1 等待中"); synchronized (o2) { system.out.println("t1 已占有 o2"); } } } }; thread t2 = new thread() { public void run() { synchronized (o2) //占有 o2 { system.out.println("t2 已占有 o2"); try { thread.sleep(1000);//停顿1000毫秒,另一个线程有足够的时间占有 o2 } catch (interruptedexception e) { e.printstacktrace(); } system.out.println("t2 试图占有 o1"); system.out.println("t2 等待中"); synchronized (o1) { system.out.println("t2 已占有 o1"); } } } }; t1.start(); t2.start(); }
t1 已占有 o1 t2 已占有 o2 t1 试图占有 o2 t1 等待中 t2 试图占有 o1 t2 等待中
线程通信
-
object 类方法
方法 描述 wait( ) 线程进入等待池 notify( ) 唤醒等待当前线程锁的线程 notifyall( ) 唤醒所有线程,优先级高的优先唤醒 为什么这些方法设置在 object 对象上?
表面上看,因为任何对象都可以加锁
底层上说,java 多线程同步的 object monitor 机制,每个对象上都设置有类似于集合的数据结构,储存当前获得锁的线程、等待获得锁的线程(lock set)、等待被唤醒的线程(wait set)
-
生产者消费者模型
- sleep 方法,让出 cpu,但不放下锁
- wait 方法,进入锁对象的等待池,放下锁
public class producerandconsumer { public static void main(string[] args) { goods goods = new goods(); thread producer = new thread()//生产者线程 { public void run() { while (true) goods.put(); } }; thread consumer = new thread()//消费者线程 { public void run() { while (true) goods.take(); } }; consumer.start(); producer.start(); } } class goods//商品类 { int num = 0;//商品数目 int space = 10;//空位总数 public synchronized void put() { if (num < space)//有空位可放,可以生产 { num++; system.out.println("放入一个商品,现有" + num + "个商品," + (space - num) + "个空位"); notify();//唤醒等待该锁的线程 } else//无空位可放,等待空位 { try { system.out.println("没有空位可放,等待拿出"); wait();//进入该锁对象的等待池 } catch (interruptedexception e) { e.printstacktrace(); } } } public synchronized void take() { if (num > 0)//有商品可拿 { num--; system.out.println("拿出一个商品,现有" + num + "个商品," + (space - num) + "个空位"); notify();//唤醒等待该锁的线程 } else///等待生产产品 { try { system.out.println("没有商品可拿,等待放入"); wait();//进入该锁对象的等待池 } catch (interruptedexception e) { e.printstacktrace(); } } } }
没有商品可拿,等待放入 放入一个商品,现有1个商品,9个空位 放入一个商品,现有2个商品,8个空位 拿出一个商品,现有1个商品,9个空位 放入一个商品,现有2个商品,8个空位 放入一个商品,现有3个商品,7个空位 放入一个商品,现有4个商品,6个空位 拿出一个商品,现有3个商品,7个空位 放入一个商品,现有4个商品,6个空位 ···
线程池
线程的启动和结束都是比较消耗时间和占用资源的,如果在系统中用到了很多的线程,大量的启动和结束动作会严重影响性能
线程池很像生产者消费者模式,消费的对象是一个一个的能够运行的任务
-
设计思路
- 准备任务容器,可用 list,存放任务
- 线程池类构造方法中创建多个执行者线程
- 任务容器为空时,所有线程 wait
- 当外部线程向任务容器加入任务,就会有执行者线程被 notify
- 执行任务完毕后,没有接到新任务,就回归等待状态
-
实现一个线程池
public class threadpool { int poolsize;// 线程池大小 linkedlist<runnable> tasks = new linkedlist<runnable>();// 任务容器 public threadpool(int poolsize) { this.poolsize = poolsize; synchronized (tasks)//启动 poolsize 个任务执行者线程 { for (int i = 0; i < poolsize; i++) { new executethread("执行者线程 " + i).start(); } } } public void add(runnable r)//添加任务 { synchronized (tasks) { tasks.add(r); system.out.println("加入新任务"); tasks.notifyall();// 唤醒等待的任务执行者线程 } } class executethread extends thread//等待执行任务的线程 { runnable task; public executethread(string name) { super(name); } public void run() { system.out.println("启动:" + this.getname()); while (true) { synchronized (tasks) { while (tasks.isempty()) { try { tasks.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } task = tasks.removelast(); tasks.notifyall(); // 允许添加任务的线程可以继续添加任务 } system.out.println(this.getname() + " 接到任务"); task.run();//执行任务 } } } public static void main(string[] args) { threadpool pool = new threadpool(3); for (int i = 0; i < 5; i++) { runnable task = new runnable()//创建任务 { public void run()//任务内容 { system.out.println(thread.currentthread().getname()+" 执行任务"); } }; pool.add(task);//加入任务 try { thread.sleep(1000); } catch (interruptedexception e) { e.printstacktrace(); } } } }
print
main 加入新任务 启动:执行者线程 0 执行者线程 0 接到任务 执行者线程 0 执行任务 启动:执行者线程 1 启动:执行者线程 2 main 加入新任务 执行者线程 2 接到任务 执行者线程 2 执行任务 main 加入新任务 执行者线程 2 接到任务 执行者线程 2 执行任务
-
java 线程池类
-
默认线程池类 threadpoolexecutor 在 java.util.concurrent 包下
threadpoolexecutor threadpool= new threadpoolexecutor(10, 15, 60, timeunit.seconds, new linkedblockingqueue<runnable>()); /* 第一个参数 int 类型, 10 表示这个线程池初始化了 10 个线程在里面工作 第二个参数 int 类型, 15 表示如果 10 个线程不够用了,就会自动增加到最多 15个 线程 第三个参数 60 结合第四个参数 timeunit.seconds,表示经过 60 秒,多出来的线程还没有接到任务,就会回收,最后保持池子里就 10 个 第五个参数 blockingqueue 类型,new linkedblockingqueue() 用来放任务的集合 */
-
execute( ) 方法添加新任务
public class testthread { public static void main(string[] args) throws interruptedexception { threadpoolexecutor threadpool= new threadpoolexecutor(10, 15, 60, timeunit.seconds, new linkedblockingqueue<runnable>()); threadpool.execute(new runnable() {//添加任务 public void run() { system.out.println("执行任务"); } }); } }
-
-
java 中几种线程池
java 线程池的*接口是 executor ,子接口是 executorservice ,子接口使用更广泛
executors 类提供了一系列工厂方法用于创建线程池,返回的线程池实现了 executorservice 接口
- newcachedthreadpool有缓冲的线程池,线程数 jvm 控制,有线程可使用时不会创建新线程
- newfixedthreadpool,固定大小的线程池,任务量超过线程数时,任务存入等待队列
- newscheduledthreadpool,创建一个线程池,可安排在给定延迟后运行命令或者定期地执行
- newsinglethreadexecutor,只有一个线程,顺序执行多个任务,若意外终止,则会新创建一个
executorservice threadpool = null; threadpool = executors.newcachedthreadpool();//缓冲线程池 threadpool = executors.newfixedthreadpool(3);//固定大小的线程池 threadpool = executors.newscheduledthreadpool(2);//定时任务线程池 threadpool = executors.newsinglethreadexecutor();//单线程的线程池 threadpool = new threadpoolexecutor(···);//默认线程池,多个可控参数
线程安全类
- stringbuffer:内部方法用 synchronized 修饰
- vetort:继承于 abstractlist
- stack:继承于 vector
- hashtable:继承于 dictionary,实现了 map 接口
- property:继承于 hashtable,实现了 map 接口
- concurrenthashmap:分段加锁机制