基本概念
并发与并行
- 并发:指两个或多个事件在同一时间间隔内发生 。当有多个线程在操作时,如果系统只有一个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 方法执行完毕
线程同步
保证程序原子性、可见性、有序性的过程
阻塞同步
基于加锁争用的悲观并发策略
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 原理
- 每个线程都有一个 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