目录
一、多线程
1、Lock锁
同步代码块和同步方法虽然解决了线程安全问题,但表达不出在哪里加了锁,在哪里释放了锁,所以在JDK1.5之后提供了一个新的锁对象:Lock
Lock是一个接口,其中有两个方法:
void lock():加锁 ;void lock():释放锁
在创建Lock对象需要使用他的实现类:ReentrantLock
以电影院卖票为例:也就是解决线程安全问题的第二种方式
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RunableDemo2 implements Runnable{
private int ticket=100;
private Lock lock=new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
lock.unlock();
}
}
}
测试类:
public class TicketDemo2 {
public static void main(String[] args) {
RunableDemo2 runableDemo2 = new RunableDemo2();
Thread t1 = new Thread(runableDemo2);
Thread t2 = new Thread(runableDemo2);
Thread t3 = new Thread(runableDemo2);
t1.setName("大黑");
t2.setName("二黑");
t3.setName("三黑");
t1.start();
t2.start();
t3.start();
}
}
输出结果和之前的结果相同
2、死锁
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
这也是同步的弊端之一:效率低;如果出现了同步嵌套,很容易产生死锁问题。
代码举例:
创建两个锁对象的类:
public class Dlock {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
}
实体类:
public class Ddemo1 extends Thread{
private boolean flag;
public Ddemo1(boolean flag){
this.flag=flag;
}
@Override
public void run() {
if(flag){
synchronized (Dlock.lock1){
System.out.println("if lock1");
synchronized (Dlock.lock2){
System.out.println("if lock2");
}
}
}else{
synchronized (Dlock.lock2){
System.out.println("else lock2");
synchronized (Dlock.lock1){
System.out.println("else lock1");
}
}
}
}
}
测试类:
public class Ddemotest {
public static void main(String[] args) {
Ddemo1 ddemo1 = new Ddemo1(true);
Ddemo1 ddemo2 = new Ddemo1(false);
ddemo1.start();
ddemo2.start();
}
}
结果:会有四种结果,两种结果正常,两种结果发生了死锁
正常结果:运行之后程序直接结束
不正常结果:发生了死锁现象:两个线程同时进入,在执行完第一个同步操作之后,由于互相之间需要的另一个锁对象正在被调用,所以两个线程在等待互相释放锁对象,因此发生了死锁,将永久等待。运行之后程序将一直运行,不会停止。
二、线程间通信
针对同一个资源的操作有不同种类的线程
举例:学生前去电影院看电影需要买票,当票卖完之后,电影院需要再次生产电影票。
按照正常分析:需要创建两个实体类去实现Runable接口,一个表示生产者,一个表示消费者,再创建一个学生对象类,表示两个实体类是对该对象进行操作,在两个实体类中都创建学生对象,表示对学生对象进行操作。
但这样输出结果永远都是默认值null和0,因为两个实体类中所进行操作的对象不是同一个对象。
改进:在外界创建一个对象,通过参数的形式传入两个实体类中进行操作。加入循环和另一个学生对象使结果更清楚:
代码实现:
实体类:get类
public class Stget implements Runnable{
private Student s;
public Stget(Student s){
this.s=s;
}
@Override
public void run() {
while(true){
System.out.println(s.name+"---"+s.age);
}
}
}
set类:
public class Stset implements Runnable{
private Student s;
private int i=0;
public Stset(Student s){
this.s=s;
}
@Override
public void run() {
while(true){
if (i%2==0){
s.name="大黑";
s.age=18;
} else{
s.name="大白";
s.age=20;
}
i++;
}
}
}
学生类:
public class Student {
public String name;
public int age;
}
测试类:
public class Stdemo1 {
public static void main(String[] args) {
// 在外界创建学生对象,通过参数的形式传入线程类中
Student s = new Student();
Stget stget = new Stget(s);
Stset stset = new Stset(s);
Thread t1 = new Thread(stget);
Thread t2 = new Thread(stset);
t1.start();
t2.start();
}
}
输出结果发现:
两个学生对象的名字和年龄会出现了对不上和重复的情况
解决办法:
满足线程安全问题的三个条件,所以需要加入synchronized关键字加锁或者Lock锁
代码完善:
加入synchronized加锁:
get类:
public class Stget implements Runnable{
private Student s;
public Stget(Student s){
this.s=s;
}
@Override
public void run() {
while(true){
synchronized (s){
System.out.println(s.name+"---"+s.age);
}
}
}
}
set类:
public class Stset implements Runnable {
private Student s;
private int i = 0;
public Stset(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (i % 2 == 0) {
s.name = "大黑";
s.age = 18;
} else {
s.name = "大白";
s.age = 20;
}
i++;
}
}
}
}
输出结果就不会出现名称与年龄不匹配的情况了,但此时出现了新的问题:一个数据操作了多次
电影院卖票是在电影院有票的情况下才会卖,若是没有票则应该先生产票在卖,消费者也是如此,卖了一张票之后,继续买票,若电影院没有票了,则应该等待电影院生产票之后再买。
这种生产消费者的问题,使用Java提供的等待唤醒机制解决
1、等待唤醒机制
如何添加等待唤醒机制呢? 所需要调用的方法在Object类中,Object类中有三个方法需要我们学习。
void notify() 唤醒正在等待对象监视器的单个线程。
void notifyAll() 唤醒正在等待对象监视器的所有线程。
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
为什么不在Thread方法中定义这三种方法:
这些方法要想调用,必须通过锁对象调用,因为如果连锁对象都不一样了,就没必要等待唤醒了,直接执行逻辑代码。 而说到现在同步代码块的锁对象是任意对象,类型无法确定,所以这些方法都定义在Object类中,因为Java所有的类都有一个共同父类Object
代码实现:
set类:
public class Stset implements Runnable {
private Student s;
private int i = 0;
public Stset(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(s.flag){//若有值,则等待消费者消费
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i % 2 == 0) {
s.name = "大黑";
s.age = 18;
} else {
s.name = "大白";
s.age = 20;
}
i++;
s.notify();
s.flag=true;
}
}
}
}
get类:
public class Stget implements Runnable{
private Student s;
public Stget(Student s){
this.s=s;
}
@Override
public void run() {
while(true){
synchronized (s){
if(!s.flag){//若没有值,则等待生产者产值
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name+"---"+s.age);
s.notify();//消费完唤醒生产者
s.flag=false;//将值改为没有值
}
}
}
}
输出结果:这样就能实现电影院生产一张票消费者就买一张票
2、线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
代码举例:
先写一个实体类实现Runnable接口,用于创建线程对象
测试类:
public class TGdemo {
public static void main(String[] args) {
TGrunnable tGrunnable = new TGrunnable();
Thread t1 = new Thread(tGrunnable, "大黑");
Thread t2 = new Thread(tGrunnable, "二黑");
//默认情况下,所有的线程属于主线程
ThreadGroup tg1 = t1.getThreadGroup();
ThreadGroup tg2 = t2.getThreadGroup();
System.out.println(tg1);
System.out.println(tg2);
//获取线程组的名称
String name1 = t1.getThreadGroup().getName();
String name2 = t2.getThreadGroup().getName();
System.out.println(name1);
System.out.println(name2);
//创建新的线程组
ThreadGroup tg3 = new ThreadGroup("白色组");
//创建对象并分组
Thread t3 = new Thread(tg3, tGrunnable, "大白");
Thread t4 = new Thread(tg3, tGrunnable, "二白");
Thread t5 = new Thread(tg3, tGrunnable, "三白");
//获取线程组的名字
System.out.println(t1.getName()+"属于线程组:"+t1.getThreadGroup().getName());
System.out.println(t2.getName()+"属于线程组:"+t2.getThreadGroup().getName());
System.out.println(t3.getName()+"属于线程组:"+t3.getThreadGroup().getName());
System.out.println(t4.getName()+"属于线程组:"+t4.getThreadGroup().getName());
System.out.println(t5.getName()+"属于线程组:"+t5.getThreadGroup().getName());
//Java允许下程序直接对线程组进行控制
//将一个线程组设置为守护线程
tg3.setDaemon(true);
}
}
输出结果:
3、线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
实现线程池
1、创建线程池对象,Executors工厂类下有很多获取线程池的静态方法。
newFixedThreadPool是其中的一个线程池
创建线程池:
public static ExecutorService newFixedThreadPool(int nThreads)
nThread指的是可存放线程的数量,若提交的线程数量多于线程池容量,则会等待直到线程池中有线程执行完毕有空闲线程位置时在执行,大体还是会执行。
线程池的一些基本操作:
存放和运行线程:
Future<?> submit(Runnable task)
提交一个可运行的任务执行,并返回一个表示该任务的未来。
即提交就运行,且底层源码将该任务封装成了一个线程对象并启动运行。
手动结束线程的运行:
void shutdown()
启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。
该方法也就是关闭线程池。
代码举例:
实现类:
public class TGrunnable implements Runnable{
@Override
public void run() {
for(int i=1;i<=3;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
测试类:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Pdemo {
public static void main(String[] args) {
TGrunnable tGrunnable = new TGrunnable();
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//提交线程并运行
pool.submit(tGrunnable);
//提交一次就是一个新的线程
pool.submit(tGrunnable);
//提交第三个线程,此时该任务仍然会执行
pool.submit(tGrunnable);
//停止线程运行,有序关闭线程池,关闭前的线程将会执行
pool.shutdown();
}
}
输出结果:
这里看出,线程2与线程1抢占调度,线程2先执行完毕,此时线程3提交到线程2位置和线程1进行抢占调度。最终全部执行完毕。
解决电影院买票线程安全问题的第三种方式:
实现Callable接口
自定义类实现Callable接口,实现call方法,该线程的启动必须与线程池结合,单独无法创建线程对象启动
实现方法和操作线程池类似
<T> Future<T> submit(Callable<T> task)
提交值返回任务以执行,并返回代表任务待处理结果的Future。
在自定义类中实现卖票的具体实现代码
代码实现:
实现类:
import java.util.concurrent.Callable;
public class CallableImpl implements Callable {
private int ticket=100;
@Override
public Object call() {
while(true){
synchronized (this){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
}
}
测试类:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Cdemo {
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3);
//创建实体类对象
CallableImpl callable = new CallableImpl();
//提交三个线程分别代表三个窗口
pool.submit(callable);
pool.submit(callable);
pool.submit(callable);
pool.shutdown();
}
}
输出结果:
Callable接口的优点:
1、可以有返回值
2、可以抛出异常
弊端:
代码比较复杂,故一般不用
匿名内部类的形式实现多线程
new Thread(){代码…}.start();继承Thread类重写run方法
New Thread(new Runnable(){代码…}).start();实现Runnable接口重写run方法
代码举例:
public class NmDemo {
public static void main(String[] args) {
//继承Thread类,重写run方法,启动线程
new Thread("大黑"){
@Override
public void run() {
for (int i=1;i<=3;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();
//实现Runnable接口,重写run方法,启动线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i=1;i<=4;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
},"二黑").start();
}
}
输出结果:
定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。 在Java中,可以通过Timer和TimerTask类来实现定义调度的功能
创建定时器:
Java提供了一个类给我们使用实现定时器:Timer
定时器中的方法:
void schedule(TimerTask task, long delay)
在指定的延迟delay之后安排指定的任务task执行。
void schedule(TimerTask task, long delay, long period)
在指定的延迟delay之后开始 ,重新执行固定延迟period执行的指定任务task。
代码举例:
import java.util.Timer;
import java.util.TimerTask;
public class Tdemo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Task1(timer),3000);
}
}
class Task1 extends TimerTask{
private Timer timer;
public Task1(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
System.out.println("beng!!!!!");
}
}
输出结果:三秒后输出beng!!!
延迟执行举例:
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo2 {
public static void main(String[] args) {
//创建定时器对象
Timer timer = new Timer();
//void schedule(TimerTask task, long delay, long period)
//在指定 的延迟之后开始 ,重新执行 固定延迟执行的指定任务。
//3秒后执行任务,并且之后每隔两秒执行一次
timer.schedule(new MyTask2(),3000,2000);
}
}
class MyTask2 extends TimerTask{
@Override
public void run() {
try {
FileReader fr = new FileReader("ddd.txt");
BufferedReader br = new BufferedReader(fr);
String s = br.readLine();
System.out.println(s);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果: