线程的创建
实现 Runnable 接口
- 重写
Runnable
接口的run
方法 Thread(Runnable target)
构造函数- 通过 Thread 代理对象调用
start
方法来启动线程
package website.yuchen;
class RunnableDemo implements Runnable
{
@Override
public void run() {
// do something
System.out.println("Hello");
}
}
public class App
{
public static void main( String[] args)
{
Runnable o = new RunnableDemo();
new Thread(o).start();
}
}
Lambda 表达式简化
因为Runnable接口只有一个方法,因此可以直接用lambda表达式简化匿名类。
package website.yuchen;
public class App
{
public static void main( String[] args)
{
/*
new Thread(new Runnable() {
@Override
public void run() {
//do something
}
}).start();
*/
new Thread(()-> {
//do something
}).start();
}
}
继承 Thread 类
继承 Thread 类,重写 run 方法,通过 start 方法调用执行。
- 继承重写 Thread 类中的 run 方法
- 通过 Thread 对象调用 start 方法
package website.yuchen;
class ThreadDemo extends Thread
{
@Override
public void run() {
// do something
}
}
public class App
{
public static void main( String[] args)
{
ThreadDemo t = new ThreadDemo();
t.start();
}
}
注:其中通过接口Runnable
创建方式是更被推崇的
线程的五种状态
- 运行态可以可以通过IO等方式进入阻塞态,而就绪态不能直接进入阻塞态
- 阻塞结束回到就绪态等待处理机调度,而不能直接进入运行态
其中同步阻塞表示线程正处于锁池状态中
常见线程阻塞方法
synchronized
:同步标识符,见下文线程同步
object.wait()
:当前运行线程进入等待阻塞,并让出临界资源Thread.sleep(millis)
:阻塞当前线程一段时间thread.join()
:让线程thread插队执行完,且当前线程阻塞(即令线程t与当前线程合并)
join 方法示例:开一个线程t和主线程main并发执行,分别从0数到99,主线程数到50时,让线程t插队执行完:
package website.yuchen;
public class App
{
public static void main(String[] args)
{
Thread t = new Thread(()-> {
for( int i = 0; i < 100; i++)
System.out.println("Thread t " + i);
}); t.start();
for( int i = 0; i < 100; i++) {
try { if( i == 50) t.join(); }
catch (InterruptedException e) {}
System.out.println("Main Thread " + i);
}
}
}
sleep 和 wait 的区别
wait的一般写法搭配:
// 线程A
synchronized (object) {
try { object.wait(); }
catch(InterruptedException e)
{ e.printStackTrace(); }
}
// 线程B
object.notify(); // 结束线程A的等待
为什么wait方法在object类中,sleep方法在Thread类中?
object.wait()
代表正在使用临界资源 object 的线程进入等待阻塞队列中,object的锁被释放,那么其他就绪的线程就可以访问这个 object 了。
此外,其他线程并不知道具体哪个线程正在使用 object,只知道 object 有没有上锁 (由object的管程控制)。所以如果用Thread.wait()来表示的话,只是代表当前线程阻塞,也无从去释放 object 的锁。
这也就是为什么 wait 方法只能放在 synchronized 代码块内。当 object.notify() 唤醒这个等待阻塞的线程时,此线程就进入锁池,在锁可用时进入就绪态。
一言以蔽之,最核心的是:同样是阻塞,wait会释放**资源锁**,但sleep不会。下述代码表现了这个特点:
package website.yuchen;
public class App
{
public static void main(String[] args)
{
Object o = new Object();
new Thread(()->
{
synchronized (o)
{
System.out.println("Thread1 is going to wait..");
try { o.wait(); }
catch (InterruptedException e)
{ e.printStackTrace(); }
System.out.println("Thread1 get up!");
}
}).start();
new Thread(()->
{
synchronized (o)
{
System.out.println("Thread2 is going to sleep..");
o.notify(); // 睡前通知Thread1起来
try { Thread.sleep(1000); }
catch (InterruptedException e)
{ e.printStackTrace(); }
System.out.println("Thread2 get up!");
}
}).start();
}
}
/* 输出结果
Thread1 is going to wait..
Thread2 is going to sleep..
Thread2 get up!
Thread1 get up!
*/
线程同步 —— synchronized关键字
以下是一个线程不安全的代码:开很多个线程往LinkedList里加元素
package website.yuchen;
import java.util.*;
public class App
{
public static void main(String[] args) throws InterruptedException
{
List<String> ls = new LinkedList<>();
Set<Thread> threads = new HashSet<>();
for(int i = 0; i < 1000; i++)
{
Thread t = new Thread(()-> {
ls.add(Thread.currentThread().getName());
}); t.start();
threads.add(t);
}
for( Thread t: threads) t.join(); // 主线程等待子线程完成
System.out.println(ls.size()); // 结果可能少于1000(线程不安全)
}
}
代码块
解决以上线程不安全问题可以给容器加同步锁 ,或者使用线程安全的容器CopyOnWriteArrayList
package website.yuchen;
import java.util.*;
public class App
{
public static void main(String[] args) throws InterruptedException
{
List<String> ls = new LinkedList<>();
// List<String> ls = new CopyOnWriteArrayList<>(); // 线程安全容器
Set<Thread> threads = new HashSet<>();
for(int i = 0; i < 1000; i++)
{
Thread t = new Thread(()-> {
synchronized (ls) { // 代码块同步锁
ls.add(Thread.currentThread().getName());
}
}); t.start();
threads.add(t);
}
for( Thread t: threads) t.join();
System.out.println(ls.size()); // 输出 1000
}
}
方法
方法的同步就相当于代码块同步锁synchronized (this)
的写法,方法同步锁写起来似乎更无脑一些。
package website.yuchen;
import java.util.*;
class MyList extends LinkedList {
@Override
public synchronized boolean add(Object o) { // 方法锁
return super.add(o);
}
}
public class App
{
public static void main(String[] args) throws InterruptedException
{
Set<Thread> threads = new HashSet<>();
List<String> ls = new MyList();
for(int i = 0; i < 1000; i++)
{
Thread t = new Thread(()-> {
ls.add(Thread.currentThread().getName());
});
t.start();
threads.add(t);
}
for( Thread t: threads) t.join();
System.out.println(ls.size()); // 输出 1000
}
}
死锁
多个并发进程因争夺系统资源而产生相互等待的现象。
四个必要条件:
- 资源只能互斥访问
- 进程自身占有资源又保持对其他资源的申请
- 进程资源不可被剥夺
- 多个进程之间形成循环等待的关系
比如这段代码多运行几次就很容易发生死锁:
package website.yuchen;
public class App
{
public static void main(String[] args)
{
Object o1 = new Object();
Object o2 = new Object();
new Thread(()->
{
synchronized (o1)
{
System.out.println("Thread1 keeps o1..");
System.out.println("Thread1 applies for o2..");
synchronized (o2) {
System.out.println("Thread1 get o2!");
}
}
}).start();
new Thread(()->
{
synchronized (o2)
{
System.out.println("Thread2 keeps o2..");
System.out.println("Thread2 applies for o1..");
synchronized (o1) {
System.out.println("Thread1 get o1!");
}
}
}).start();
}
}
生产消费者模型
生产消费者模型是多线程并发中的一个经典问题。比如在食堂排队取餐,后厨厨师进行生产,与此同时学生进行购买消费。这就需要做到多线程下的资源协调。
class Resource {
int id;
Resource(int id) { this.id = id; }
}
class Producer {
Queue<Resource> buffer;
Producer(Queue buffer) { this.buffer = buffer; }
static int ver = 0;
public synchronized void produce() { // 多个生产者之间的同步锁
synchronized (buffer) { // 生产者与消费者之间打印的信息协调
buffer.offer(new Resource(++ver));
System.out.println("Producer produce resource " + ver);
}
}
}
class Consumer {
Queue<Resource> buffer;
Consumer(Queue buffer) { this.buffer = buffer; }
public synchronized void consume() {
synchronized (buffer) {
Resource t = buffer.poll();
System.out.println("Consumer consume resource " + t.id);
}
}
}
生产者和消费者之间交互通过缓冲区来解耦
借助wait和notify方法实现,这两个方法需要处于synchronized范围内
- object.wait():当前运行线程进入等待阻塞,并让出临界资源
- object.notifyAll():唤醒全部等待池中的线程
class buffererQueue extends LinkedList
{
static int MAXN = 20;
@Override
public synchronized boolean offer(Object o) // 这里不加synchronized也可以,因为调用前已对buffer上锁
{
while( this.size() >= MAXN) { // 缓冲区满,生产者阻塞等待
try { this.wait(); }
catch (InterruptedException e)
{ e.printStackTrace(); }
} // 使用 while 比 if 更安全
boolean res = super.offer(o);
this.notifyAll(); // 生产者生产了资源,唤醒等待的消费者
return res;
}
@Override
public synchronized Object poll()
{
while( this.size() <= 0) { // 缓冲区空,消费者阻塞等待
try { this.wait(); }
catch (InterruptedException e)
{ e.printStackTrace(); }
}
Object res = super.poll();
this.notifyAll(); // 消费者消耗了资源,唤醒等待的生产者
return res;
}
}
以下是多对多的生产消费者模型:生产者和消费者各100个,每个生产者生产1000份,消费者消耗1000份,协同运作:
public class App
{
public static void main(String[] args)
{
Queue<Resource> buffer = new buffererQueue();
Producer producer = new Producer(buffer);
Consumer consumer = new Consumer(buffer);
for( int k = 0; k < 100; k++) {
new Thread(()-> {
for( int i = 0; i < 1000; i++)
producer.produce();
}).start();
}
for( int k = 0; k < 100; k++) {
new Thread(()-> {
for( int i = 0; i < 1000; i++)
consumer.consume();
}).start();
}
}
}
其他
线程优先级
thread.setPriority(newPriority)
thread.getPriority()
优先级 newPriority 介于1~10之间,默认是5
优先级高的线程并不代表能独占cpu,而是宏观上来看被调度的几率更大。
由于Java语言层面的线程的实际上还会被映射到对应OS的原生线程上,因此Jvm主流的线程的调度方式取决于OS所支持的线程模型。
简单拓展一下:比如历史上Linux系统曾采用 1:1
的线程模型,即一个轻量级进程(即用户线程)对应一个内核线程,由线程调度器来分配cpu资源,而此时Jvm对于线程的操作是通过映射到用户线程实现的,也是 1:1
的关系。
守护线程
Java守护线程为用户线程而服务,但用户线程全部结束时,守护线程即使还有语句没能执行也会随之结束。
常用于程序日志打印等服务
thread.setDaemon(true)
package website.yuchen;
public class App
{
public static void main(String[] args)
{
Thread guard = new Thread(()-> {
while(true) System.out.println("Daemon");
});
guard.setDaemon(true);
guard.start();
new Thread(()-> {
for( int i = 0; i < 1000; i++)
System.out.println("Thread");
}).start();
}
}
Java 多线程内存模型
在Java中每个线程都有一个工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝。
这就可能会导致不同工作内存中的数据不同的问题。
保证数据的同步性 —— volatile 关键字
volatile
是一种轻量级的锁,它能保证锁的可见性,但不能保证锁的原子性(synchronized既能保证原子性又能保证可见性)。
也就是用volatile修饰的变量每次使用读取的都会是直接与主存同步的最新的值。
以下是一个volatile的经典应用,如果去掉flag变量的volatile标识则很可能会陷入死循环:
package website.yuchen;
public class App
{
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException
{
new Thread(()-> {
while(flag);
}).start();
Thread.sleep(500);
flag = false;
}
}
发生死循环的原因是因为Jvm对于这种反复高密度的简单指令优化更加激进(指令重排),从而无暇去同步工作内存中所对应的值。
如果循环中出现比较复杂的算法或慢速指令比如计算随机数、打印输出等就不会出现死循环。volatile还有一种经典的应用就是配合单例模式的实现,是为了防止创建对象时指令重排,这里就不展开了。
要点:
与主存直接同步
禁止指令重排
- 不保证语句原子性
- 修饰变量
可重入锁
可重入就是一个线程可以多次获取同一个锁
比如因为可重入性,以下代码并不会产生死锁:
synchronized (this) {
synchronized (this) {
// ...
}
}
ReentrantLock 用法示意
ReentrantLock
是一种可重入的锁,包含synchronized关键字的功能且提供许多其他功能。
class Foo {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void bar() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
可重入锁的实现
对锁记录正在使用的线程以及该线程重入次数的计数
package website.yuchen;
/* 不可重入锁
class Lock
{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException
{
while(isLocked) wait();
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}*/
class Lock
{
private boolean isLocked = false;
private int cnt = 0;
private Thread thread = null;
public synchronized void lock() throws InterruptedException
{
Thread tcur = Thread.currentThread();
while(isLocked && thread != tcur) wait();
cnt ++; thread = tcur;
isLocked = true;
}
public synchronized void unlock() {
if( --cnt > 0) return ;
isLocked = false; thread = null;
notify();
}
}
public class App
{
public static void main(String[] args) throws InterruptedException
{
Lock lock = new Lock();
lock.lock();
{
System.out.println("Main Thread get lock.");
lock.lock();
System.out.println("Main Thread get lock again..");
lock.unlock();
}
lock.unlock();
}
}
以上为个人学习总结,如有问题欢迎提出。