承上启下
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
一、JDK5中Lock锁的使用
void lock() 上锁
void unlock() 释放锁
代码示意:
public class SellTicket implements Runnable {
private int ticket = 20;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if (ticket <= 0) {
break;
}
//卖票这个动作不安全。
System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "票");
lock.unlock();
System.out.println(Thread.currentThread().getName()+"结束");
}
}
}
首先我们要造一个锁
Lock lock = new ReentrantLock();
然后调用lock.lock()和lock.unlock()将需要上锁的代码包起来。
但是查看java的一些源码,还是synchronized用的多。
虽然线程有了锁解决了安全问题,但是偶尔也会因为失误操作出现死锁的情况。
同步弊端:
- 效率低
- 如果出现了同步嵌套,就容易产生死锁问题
什么是死锁:
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。
示例:
首先造两个锁:
public class MyLock {
// 创建两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}
同步代码块嵌套:
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
测试:
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
运行打印:
if objA
else objB
二、线程间通信
生产者、消费者模式:
生产者没有就生产,有就等待消费者消费;消费者有就消费,没有就等待生产者生产。
java提供了等待唤醒的机制。
Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程
代码示例:
public class Student {
String name;
int age;
boolean flag;
}
生产者:
public class SetThread implements Runnable {
private Student s;
public SetThread(Student s){
this.s = s;
}
private int x = 0;
@Override
public void run() {
while (true){
synchronized (s) {
if (s.flag){
try {
s.wait();//t1等着,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.age=20;
s.name="徐繁韵";
} else {
s.age=21;
s.name="唐富平";
}
x++;
s.flag=true;
s.notify();//唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
}
//t1有,或者t2有
}
}
}
消费者:
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s){
this.s = s;
}
@Override
public void run() {
while (true){
synchronized (s) {
if (!s.flag){
try {
s.wait();//t2等待,立即释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + ":" + s.age);
s.flag=false;
s.notify();//唤醒t1,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
}
}
}
}
测试:
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
输出打印:
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
。
。
。
看的出是生产一条消费一条。为了实现线程间的通信,将共同操作的数据通过有参构造器传入线程。
思考一个问题,为什么等待唤醒的方法不定义在Thread里呢?
这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。所以,这些方法必须定义在Object类中。
栗子优化:
既然wait()、notify()、notifyAll()定义在锁对象里,那么我们把前面的栗子优化一下。
把Student的成员变量给私有化,把设置和获取的操作给封装成功能,并加上同步。设置或者获取的线程里面只需要调用方法即可。
public class Student {
private String name;
private int age;
boolean flag;
public synchronized void set(String name,int age){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.age = age;
this.flag = true;
this.notify();
}
public synchronized void get(){
if (!this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name+":"+this.age);
this.flag = false;
this.notify();
}
}
public class SetThread implements Runnable {
private Student s;
public SetThread(Student s){
this.s = s;
}
private int x = 0;
@Override
public void run() {
while (true){
if (x % 2 == 0) {
s.set("徐繁韵",20);
} else {
s.set("唐富平",21);
}
x++;
}
}
}
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s){
this.s = s;
}
@Override
public void run() {
while (true){
s.get();
}
}
}
public class StudentDemo {
public static void main(String[] args) {
Student s = new Student();
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
线程的状态转换图:
三、线程组:
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
查看我们平时创建的线程默认是属于哪个组:
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my,"线程一");
Thread t2 = new Thread(my,"线程二");
System.out.println(t1.getThreadGroup().getName());//main
System.out.println(t2.getThreadGroup().getName());//main
System.out.println(Thread.currentThread().getThreadGroup().getName());//main
可以看出主线程和我们创建的线程都默认属于main线程组。
接下来我们自定义线程组:
//创建一个线程组
ThreadGroup tg = new ThreadGroup("dev");
MyRunnable my = new MyRunnable();
创建线程时分配组
Thread t1 = new Thread(tg,my,"线程一");
Thread t2 = new Thread(tg,my,"线程二");
System.out.println(t1.getThreadGroup().getName());//dev
System.out.println(t2.getThreadGroup().getName());//dev
线程组可以统一管理:
tg.setDaemon(true);
tg.interrupt();
tg.destroy();
tg.isDestroyed();
tg.isDaemon();
四、线程池:
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
JDK5新增了一个Executors工厂类来产生线程池
有如下几个方法:
public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
代码示意:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
//创建线程池,大小为2
ExecutorService pool = Executors.newFixedThreadPool(2);
//将实现了接口Runnable的线程放到线程池里运行
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.shutdown();//关闭线程池
}
}
因为线程池的出现,实现线程的方式有了第三种。
实现Callable接口
public class MyCallable implements Callable {
private String name;
public MyCallable(String name){
this.name = name ;
}
@Override
public Object call() throws Exception {
Thread.currentThread().setName(this.name);
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
return null;
}
}
public class ThreadCallableDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(new MyCallable("call一号"));
pool.submit(new MyCallable("call二号"));
pool.shutdown();
}
}
这种方式必须依赖线程池实现。可以看出在线程池中,Runnable和Callable两种方式基本相同,不同的是Callable接口是支持泛型的,call()也是有返回值,返回值类型是泛型的类型。
简单应用:
//计算1-n之后
public class MyCallable implements Callable<Integer> {
private Integer num;
public MyCallable(Integer num){
this.num = num;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <=num; i++) {
sum +=i;
}
return sum;
}
}
public class ThreadCallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2= pool.submit(new MyCallable(200));
System.out.println(f1.get());
System.out.println(f2.get());
pool.shutdown();
}
}
五、匿名内部类方式使用多线程
在现实使用中,有时候没必要新建一个实现Runnable接口的类来创建线程,我们可能把线程用完就丢了,这样就用到了匿名内部类方式的线程。
代码示意:
/*
* 匿名内部类的格式:
* new 类名或者接口名() {
* 重写方法;
* };
* 本质:是该类或者接口的子类对象。
*/
public class ThreadNimingDemo {
public static void main(String[] args) {
// 继承Thread类来实现多线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}.start();
// 实现Runnable接口来实现多线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}){}.start();
//高难度的错误示范
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello"+"---"+i);
}
}
}){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("word"+"---"+i);
}
}
}.start();//执行的是word,这个算是面试题吧,但是这是错误用法,现实中不会出现的。
}
}
六、多线程总结:
在多线程的面试中经常会问到这些问题:
1:多线程有几种实现方案,分别是哪几种?
两种。
继承Thread类
实现Runnable接口扩展一种:实现Callable接口。这个得和线程池结合。
2:同步有几种方式,分别是什么?
两种。
同步代码块
同步方法
3:启动一个线程是run()还是start()?它们的区别?
start();
run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法
4:sleep()和wait()方法的区别
sleep():必须指定时间;不释放锁。
wait():可以不指定时间,也可以指定时间;释放锁。
5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中
因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。
6:线程的生命周期图
新建 -- 就绪 -- 运行 -- 死亡
新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡
建议:画图解释。
此次线程的学习,只是简单的涉及,线程间的通信、线程池等并没有深入探究。在经后实际项目的高并发的解决措施中再做详细讲述。此次回顾只是为下一步高并发的研究做基础准备。
此次笔记略显粗糙,欢迎批评指正,互相学习。
源码码云地址:
https://gitee.com/stefanpy/java