0
点赞
收藏
分享

微信扫一扫

java基础thread——java5之后的多线程(浅尝辄止)


承上启下

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,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基础thread——java5之后的多线程(浅尝辄止)_ide

三、线程组:

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


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两种方式基本相同,不同的是Callage接口是支持泛型的,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:线程的生命周期图


新建 – 就绪 – 运行 – 死亡

新建 – 就绪 – 运行 – 阻塞 – 就绪 – 运行 – 死亡

建议:画图解释。


此次线程的学习,只是简单的涉及,线程间的通信、线程池等并没有深入探究。在经后实际项目的高并发的解决措施中在做详细讲述。此次回顾只是为下一步高并发的研究做基础准备。

此次笔记略显粗糙,欢迎批评指正,互相学习。

源码码云地址:

​​

​​

​​​​



举报

相关推荐

0 条评论