目录
第一章 多线程
1.1 线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
测试类:
package day27.demo1;
/*
* 多线程并发访问同一个数据资源
* 3个线程,对一个票资源,出售
*/
public class ThreadDemo {
public static void main(String[] args) {
//创建Runnable接口实现类对象
Tickets t = new Tickets();
//创建3个Thread类对象,传递Runnable接口实现类
Thread t0 = new Thread(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t0.start();t1.start();t2.start();
}
}
模拟票:
package day27.demo1;
public class Tickets implements Runnable{
//定义出售的票源
private int ticket = 100;
private Object obj = new Object();
public void run(){
while(true){
//对票数判断,大于0,可以出售,变量--操作
if( ticket > 0){
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
}
}
运行结果:
run方法只有一个,但要进栈3次,使用的是同一个票源ticket。假如票剩1张是,t0先抢占了cpu,刚判断完ticket>0,但还未执行ticket--时,cpu被t1抢占,此时ticket依然大于0。此时这种情况会导致最后输出的ticket出现负数。这是线程不安全。
若我们用休眠来演示cpu被抢占:
只需要修改模拟票类:
package day27.demo1;
/*
* 通过线程休眠,出现安全问题
*/
public class Tickets implements Runnable{
//定义出售的票源
private int ticket = 100;
private Object obj = new Object();
public void run(){
while(true){
//对票数判断,大于0,可以出售,变量--操作
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
}
}
运行结果:
1.2 线程同步(线程安全处理Synchronized)
java中提供了线程同步机制,它能够解决上述的线程安全问题。
线程同步的方式有两种:
方式1:同步代码块
方式2:同步方法
1.2.1 同步代码块
同步代码块: 在代码块声明上,加上synchronized。
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
同步代码块中的锁对象可以是任意的对象;
此对象不能写匿名对象,因为每次while循环,匿名对象就变了,因此必须写有名对象。即多个线程时,要使用同一个锁对象才能够保证线程安全。
使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:
package day27.demo1;
/*
* 通过线程休眠,出现安全问题
* 解决安全问题,Java程序,提供技术,同步技术
* 公式:
* synchronized(任意对象){
* 线程要操作的共享数据
* }
* 同步代码块
*/
public class Tickets implements Runnable{
//定义出售的票源
private int ticket = 100;
private Object obj = new Object();
public void run(){
while(true){
//线程共享数据,保证安全,加入同步代码块
synchronized(obj){
//对票数判断,大于0,可以出售,变量--操作
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
}
}
}
运行结果:
1. 线程遇到同步代码块后,线程判断还有没有同步锁。若有同步锁,则此线程获取锁,进入同步代码块中,去执行代码,执行完毕后,出同步代码块,并且将锁对象还回去。 若没有锁,则此线程被挡在同步代码块的外面。
2. 通过运行我们明显感觉到执行速度变慢。这是因为这是线程安全的,执行速度会变慢。
因为有了同步之后,线程每次要判断同步锁,获取锁,使用完之后还有释放锁,导致程序运行速度降低。
1.2.2 同步方法(建议使用)
同步方法:在方法声明上加上synchronized
比同步代码块好,代码量少。
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法中,锁对象是 this。
//同步方法中的对象锁,是本类对象引用 this
public void payTicket(){
//静态方法中的成员也要为静态,因此ticket设为静态
synchronized(this) {
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
}
静态里面不能写this和super,因为静态是类的共享数据,不属于对象。而this属于对象引用,并且静态优先于非静态存在。
静态的同步方法中,锁对象是本类类名.class。
//静态方法,同步锁,是本类类名.class属性
public static void payTicket(){
//静态方法中的成员也要为静态,因此ticket设为静态
synchronized(Tickets1.class)//与反射有关 {
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
}
使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:
package day27.demo1;
/*
* 采用同步方法形式,解决线程的安全问题
* 好处: 代码简洁
* 将线程共享数据,和同步,抽取到一个方法中
* 在方法的声明上,加入同步关键字
*
* 问题:
* 同步方法有锁吗,肯定有,同步方法中的对象锁,是本类对象引用 this
* 如果方法是静态的呢,同步有锁吗,绝对不是this
* 锁是本类自己.class 属性
* 静态方法,同步锁,是本类类名.class属性
*/
public class Tickets1 implements Runnable{
//定义出售的票源
private static int ticket = 100;
public void run(){
while(true){
payTicket();
}
}
public synchronized void payTicket(){
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
// //静态方法,同步锁,是本类类名.class属性
// public static void payTicket(){
//
// //静态方法中的成员也要为静态,因此ticket设为静态
// synchronized(Tickets1.class)//与反射有关 {
// if( ticket > 0){
// try{
// Thread.sleep(10);
// }catch(Exception ex){}
// System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
// }
// }
//}
// //同步方法中的对象锁,是本类对象引用 this
// public void payTicket(){
// //静态方法中的成员也要为静态,因此ticket设为静态
// synchronized(this) {
// if( ticket > 0){
// try{
// Thread.sleep(10);
// }catch(Exception ex){}
// System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
// }
// }
//}
}
1.3 Lock接口
public synchronized void payTicket(){
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
此代码中,有以下两个问题:
1. 何时获取锁我们知道,但是何时释放锁,我们并不知道。
2.若在sleep过程中,真的发生了异常,catch后此线程执行结束,但是锁没有释放,因为不出同步块,锁不释放。
因此出现了Lock来替换synchronized。
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的常用方法:
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:
package day27.demo1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 使用JDK1.5 的接口Lock,替换同步代码块,实现线程的安全性
* Lock接口方法:
* lock() 获取锁
* unlock()释放锁
* 实现类ReentrantLock
*/
public class Tickets_2 implements Runnable{
//定义出售的票源
private int ticket = 100;
//在类的成员位置,创建Lock接口的实现类对象
//写在run中不可(若有多个线程,则有多个锁,此应为共享资源),写在while循环中每次循环生成的锁不同。
private Lock lock = new ReentrantLock();
public void run(){
while(true){
//调用Lock接口方法lock获取锁
lock.lock();
//对票数判断,大于0,可以出售,变量--操作
if( ticket > 0){
try{
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}catch(Exception ex){
}finally{
//释放锁,调用Lock接口方法unlock
lock.unlock();
}
}
}
}
}
1.4 死锁
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
synchronzied(A锁){
synchronized(B锁){
}
}
外部类要访问非静态成员需要对象调,但是此方法为私有的,外部类不能创建对象。因此外部类可以静态调,不需要对象。
1. 定义锁对象类
package day27.demo1;
public class LockA {
private LockA(){}
public static final LockA locka = new LockA();
}
package day27.demo1;
public class LockB {
private LockB(){}
public static final LockB lockb = new LockB();
}
2.线程任务类
package day27.demo1;
public class DeadLock implements Runnable{
private int i = 0;
public void run(){
while(true){
//随着循环次数增多,发生死锁的几率就增加了
if(i%2==0){
//先进入A同步,再进入B同步
synchronized(LockA.locka){
//外部类不能创建对象,但是外部类可以静态调 LockA.locka
System.out.println("if...locka");
synchronized(LockB.lockb){
System.out.println("if...lockb");
}
}
}else{
//先进入B同步,再进入A同步
synchronized(LockB.lockb){
System.out.println("else...lockb");
synchronized(LockA.locka){
System.out.println("else...locka");
}
}
}
i++;
}
}
}
3.测试类
package day27.demo1;
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock dead = new DeadLock();
Thread t0 = new Thread(dead);
Thread t1 = new Thread(dead);
t0.start();
t1.start();
}
}
运行结果:
1.5 等待唤醒机制
线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:
- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
- notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?
因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
建在while循环内不对,没次循环,建立不同的Resource对象。 建在run内也不合适,若有三个线程,三个run,每个run都有自己的Resource,不合适。Resource应该为共享数据。
输出null..null的解决方法:
方法一:死锁中的静态调用
方法二:在测试类中创建一个对象,给input和output传参。
但依然出现问题,lisi性别是nv,但打印出为男。
性别与名字不匹配的问题,解决方法:写和读都要加锁,都为资源锁,不能为this锁。
需要锁的对象调用wait()
输入线程任务类:
package day27.demo1;
/*
* 输入的线程,对资源对象Resource中成员变量赋值
* 一次赋值 张三,男
* 下一次赋值 lisi,nv
*/
public class Input implements Runnable {
private Resource r ;
public Input(Resource r){
this.r = r;
}
public void run() {
int i = 0 ;
while(true){
synchronized(r){
//标记是true,等待
if(r.flag){
try{r.wait();}catch(Exception ex){}
}
if(i%2==0){
r.name = "张三";
r.sex = "男";
}else{
r.name = "lisi";
r.sex = "nv";
}
//将对方线程唤醒,标记改为true
r.flag = true;
r.notify();
}
i++;
}
}
}
输出线程任务类:
package day27.demo1;
/*
* 输出线程,对资源对象Resource中成员变量,输出值
*/
public class Output implements Runnable {
private Resource r ;
public Output(Resource r){
this.r = r;
}
public void run() {
while(true){
synchronized(r){
//判断标记,是false,等待
if(!r.flag){
try{r.wait();}catch(Exception ex){}
}
System.out.println(r.name+".."+r.sex);
//标记改成false,唤醒对方线程
r.flag = false;
r.notify();
}
}
}
}
模拟资源类:
package day27.demo1;
/*
* 定义资源类,有2个成员变量
* name,sex
* 同时有2个线程,对资源中的变量操作
* 1个对name,age赋值
* 2个对name,age做变量的输出打印
*/
public class Resource {
public String name;
public String sex;
public boolean flag = false;
}
测试类:
package day27.demo1;
/*
* 开启输入线程和输出线程,实现赋值和打印值
*/
public class ThreadDemo3{
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out);
tin.start();
tout.start();
}
}
运行结果:
第二章 总结
同步锁
多个线程想保证线程安全,必须要使用同一个锁对象
同步代码块
synchronized (锁对象){
可能产生线程安全问题的代码
}
同步代码块的锁对象可以是任意的对象
同步方法
public synchronized void method()
可能产生线程安全问题的代码
}
同步方法中的锁对象是 this
静态同步方法
public synchronized void method()
可能产生线程安全问题的代码
}
静态同步方法中的锁对象是 类名.class
多线程有几种实现方案,分别是哪几种?
a, 继承Thread类
b, 实现Runnable接口
c, 通过线程池,实现Callable接口
同步有几种方式,分别是什么?
a,同步代码块
b,同步方法
静态同步方法
为什么wait(),notify(),notifyAll()等方法都定义在Object类中
锁对象可以是任意类型的对象