并发与多线程
2019-12-23 08:56:39来源:博客园 阅读 ()
并发与多线程
并发与多线程
基本概念
并发与并行
- 并发:指两个或多个事件在同一时间间隔内发生 。当有多个线程在操作时,如果系统只有一个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:分段加锁机制
原文链接:https://www.cnblogs.com/pgjett/p/12082133.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:“死锁”四个必要条件的合理解释
下一篇:如何写出没有BUG的代码
- 最详细的java多线程教程来了 2020-06-08
- 系统化学习多线程(一) 2020-06-08
- 多线程:生产者消费者(管程法、信号灯法) 2020-06-01
- 如何合理地估算线程池大小? 2020-05-31
- 那些面试官必问的JAVA多线程和并发面试题及回答 2020-05-28
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash