虽然我们可以理解同步代码块和同步方法锁对象的问题,但是我们没有直接看到在哪加了锁,在哪里释放了锁,为了更直观的加锁和释放锁,jdk5以后提供了一个新的锁对象Lock
之前那个卖票的例子,用Lock实现
public class MyRunable implements Runnable {
int p = 100;
private Lock lock=new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//加锁
lock.lock();
if (p > 0) {
try {
// 模拟延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p--;
System.out.println("卖了" + (100 - p) + "张票,还剩" + p + "张" + "----" + Thread.currentThread().getName());
}else {
break;
}
} finally {
// 释放锁
lock.unlock();
}
}
}
同步的弊端:
A:效率低
B:如果产生了同步嵌套,容易产生死锁问题。
死锁问题:是指多个线程在执行过程中,因争夺资源而产品的互相等待的问题。
举个列子
public class MyThread extends Thread{
boolean flag;
public MyThread(boolean flag) {
this.flag=flag;
}
@Override
public void run() {
if(flag) {
synchronized (DieLock.obja) {
System.out.println("if obja");
synchronized (DieLock.objb) {
System.out.println("if objb");
}
}
}else {
synchronized (DieLock.objb) {
System.out.println("if objb");
synchronized (DieLock.obja) {
System.out.println("if obja");
}
}
}
}
public static void main(String[] args) throws Exception {
MyThread thread1=new MyThread(true);
MyThread thread2=new MyThread(false);
thread1.start();
thread2.start();
}
}
以上代码就会产生死锁问题。
解析:当thread1线程进入if 锁住了obja对象,而同时thread2也进入了else 锁住了objb对象。而thread1想要往下走必须等待objb的对象的释放,而thread2往下走也必须等待obja对象的释放。从而两个线程进入了互相等待锁释放,从而形成了死锁。
线程之间的通信问题。
1、同一个资源类
2、生产者(设置资源)
3、消费者(获取资源)
举个例子:
1、资源类Student
2、生产者SetThread对Student设置属性
3、消费者GetThread获取Student属性
public class Student {
String name;
int age;
}
public class SetThread implements Runnable {
Student student;
int x=0;
public SetThread(Student student) {
this.student=student;
}
@Override
public void run() {
while(true) {
if(x%2==0) {
student.name="杨过";
student.age=18;
}else {
student.name="郭靖";
student.age=30;
}
x++;
}
}
}
public class GetThread implements Runnable {
Student student;
public GetThread(Student student) {
this.student=student;
}
@Override
public void run() {
while(true) {
System.out.println(student.name+"-----"+student.age);
}
}
}
public class Demo {
public static void main(String[] args) {
Student student = new Student();
SetThread set=new SetThread(student);
GetThread get=new GetThread(student);
Thread t1=new Thread(set);
Thread t2=new Thread(get);
t1.start();
t2.start();
}
}
上面的代码就是setThread根据x取模的值,不停的设置student的属性,而getThread就是不停的获取student的属性
运行发现出现了异常情况:
输出数据不但出现了重复,而且名字和年龄也匹配不上。
解析:出现了多次同样的数据,很好解释,因为getThread的抢到了多次执行权,所以还没来得级设置就打印了多次。
名字和年龄匹配不上是因为:setThread进入run方法,在设置名称完毕,刚准备设置年龄时就被getThread抢到了执行权,所以getThread获取到的是setThread设置完毕的名字和setThread还没来得及设置的年龄。所以就出现了年龄和名字不匹配。
修改代码:
@Override
public void run() {
while(true) {
synchronized (Student.class) {
if(x%2==0) {
student.name="杨过";
student.age=18;
}else {
student.name="郭靖";
student.age=30;
}
x++;
}
}
}
@Override
public void run() {
while(true) {
synchronized (Student.class) {
System.out.println(student.name+"-----"+student.age);
}
}
}
这里就加了锁,就不会出现名字和年龄不匹配了。注意这里也要对消费者加锁,而且要加同一锁。
之前我没对消费者(getThread)加锁,还是会出现年龄和名字不匹配的问题。还是对锁不太熟悉==!。
解析:如果只对生产者加了锁,这时你并没有把getThread加入到同步代码块中,它还是可以抢占setThread的cpu使用权,从而出现了年龄和名字不匹配的问题。如果对消费者加了锁,它就必须等待Student.class锁的释放(等待setThread的设置属性的代码执行完毕),才能打印值,这样就不会出现年龄和名字不匹配的问题了。
上面的代码还存在着一下问题:
1、如果第一个执行的是消费者,那么生产者还没有设置数据。消费者去获取数据显然也是无意义的,应该等待着生产者生产完数据再去获取数据。
2、如果第一次执行的是生产者,生产完数据后,他还拥有着执行权,继续生产数据,将之前的数据覆盖了,显然也是不合理的。应该等消费者消费完,然后再生产。
正常的思路:
A:生产者
看是否有数据,有就等待,没有就生产,生产完后通知消费者来消费数据。
B:消费者
先看是否有数据,有就消费,没有就等待,通知生产者生产数据。
为了解决这样的问题,java提供了一种机制:等待唤醒机制。
将代码改成如下:
public class Student {
String name;
int age;
boolean flag;
}
public class SetThread implements Runnable {
Student student;
int x=0;
public SetThread(Student student) {
this.student=student;
}
@Override
public void run() {
while(true) {
synchronized (student) {
if(student.flag) {
try {
student.wait();//等待并释放锁。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(x%2==0) {
student.name="杨过";
student.age=18;
}else {
student.name="郭靖";
student.age=30;
}
x++;
student.flag=true;
student.notify();//唤醒
}
}
}
public class GetThread implements Runnable {
Student student;
public GetThread(Student student) {
this.student=student;
}
@Override
public void run() {
while(true) {
synchronized (student) {
if(!student.flag) {
try {
student.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(student.name+"-----"+student.age);
student.flag=false;
student.notify();//唤醒
}
}
}
}
解析:假如消费者先抢到了线程,student.flag此时为false这样就会执行student.wait();线程等待释放锁,此时生产者就会获得Cpu执行权,然后设置值,并设置flag为true,执行student.notify();唤醒线程,然后生产者和消费者就会抢CPU的执行权,假设生产者又一次抢到了,那么执行student.wait();进入等待,消费者又会拿到执行权,并打印数据。
注意事项:
1、唤醒和等待一定要有锁对象的wait和notify方法,所以我将锁对象Student.class改成了student对象。
2、wait方法,除了让当前线程进入阻塞状态,还会释放锁。一旦被唤醒也是从wait所在的行往下执行,而不是从头开始执行。
线程组:java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,java允许程序直接对线程组控制。默认情况下所有的线程都属于同一个线程组。
1、获得线程组
MyRunable runbable1 = new MyRunable();
Thread thread1 = new Thread(runbable1,"线程1");
Thread thread2 = new Thread(runbable1,"线程2");
ThreadGroup group1=thread1.getThreadGroup();
ThreadGroup group2=thread2.getThreadGroup();
System.out.println(group1.getName()+"---"+group2.getName());
2、修改线程组
ThreadGroup gp=new ThreadGroup("hello");
MyRunable runbable1 = new MyRunable();
Thread thread1 = new Thread(gp,runbable1,"线程1");
Thread thread2 = new Thread(gp,runbable1,"线程2");
ThreadGroup group1=thread1.getThreadGroup();
ThreadGroup group2=thread2.getThreadGroup();
System.out.println(group1.getName()+"---"+group2.getName());
3、可以通过线程组设置一些东西
gp.setDaemon(true);//通过线程组设置这个组的线程都是守护线程
线程池:程序启动一个新的线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中称为空闲状态,等待下一个对象来使用。
创建线程池,执行,并销毁。(Runable方式)
//创建有2个线程的线程池
ExecutorService pool=Executors.newFixedThreadPool(2);
// 执行Runnable对象或者Callable对象
pool.submit(new MyRunable());
pool.submit(new MyRunable());
pool.submit(new MyRunable());
//结束线程池
pool.shutdown();
创建线程池,执行,并销毁。(Callable方式)
需求线程一求0-99的和,线程二求0-199的和
public class MyCallable implements Callable {
private Integer endNum;
public MyCallable(Integer endNum) {
this.endNum=endNum;
}
@Override
public Integer call() throws Exception {
int res=0;
for (int i = 0; i < endNum; i++) {
res+=i;
System.out.println(res);
}
return res;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool=Executors.newFixedThreadPool(2);
Future future1=pool.submit(new MyCallable(100));
Future future2=pool.submit(new MyCallable(200));
System.out.println(future1.get()+"----"+future2.get());
pool.shutdown();
}
}
匿名内部类启动多线程
Thread
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}.start();
Runnable
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}).start();
面试题:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello"+i);
}
}
}) {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("world"+i);
}
}
}.start();
这样会打印什么?
打印world,还是走的子类对象。
定时器:略(一般开发中用quarz做定时任务)
实现多线程有几种方式?
1、继承Thread类,重写run方法
2、实现runnable接口,重写run方法
3、实现callable接口,重写run方法,依赖于线程池使用
同步有几种方法
1、同步代码块
2、同步方法
启动一个线程调用run还是start,它们的区别?
调用start,调用run仅仅相当于调用一个普通方法,调用start会启动一个线程再调用run方法。
sleep和wait的区别
sleep必须指定睡眠多少秒,且不会释放锁,而wait可以不指定等待多少秒,且会释放锁。
为什么wait和notify,notifyAll是Object类的。
因为这些方法是依赖于锁对象的。