一、线程的生命周期
JDK中用Thread.State类定义了线程的几种状态 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能。
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
二、线程同步
1、线程同步的引入问题:三个窗口同时售卖总共100张火车票。
2、问题的发现:使用继承Thread类的方式与使用实现Runnable接口的方式都会出现错票、重票等问题。
①、使用继承Thread类的方式
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(getName() + "卖出票,剩余:" + ticket);
ticket--;
}else{
return;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
此时,运行程序会发现出现了重票、错票等问题。
②、使用实现Runnable接口的方式
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为:" + ticket + "张票");
ticket--;
}else{
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
此时,运行程序会发现出现了重票、错票等问题。
3、总结:
①.多线程出现了安全问题
②. 问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
③. 解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
4、Java的SDK提供了同步机制解决以上线程不安全的办法,分别为:
①同步代码块
synchronized (对象){
// 需要被同步的代码;
}
以三个窗口卖票问题为例使用同步代码块分别解决使用继承Thread类与实现Runnable接口方式的线程不安全问题:
a.使用继承Thread类
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){//Class clazz = Window.class; Window.class表示当前类,只加载一次
//错误的:因为此时this代表t1、t2、t3
// synchronized(this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + "卖出票,剩余:" + ticket);
ticket--;
}else{
return;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w1 = new Window2();
Window2 w2 = new Window2();
Window2 w3 = new Window2();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
运行结果:不会出现错票、重票
补充:通过继承Thread类来解决线程安全问题时,可以考虑使用任何一个类的对象(Dog dog = new Dog())或者线程类本身(Window2.class)来充当同步监视器。
b、实现Runnable接口方式
class Window1 implements Runnable {
private int ticket = 100;
// Object obj = new Object();
// Dog dog = new Dog();
@Override
public void run() {
// Object obj = new Object();
while (true) {
synchronized(this){
//synchronized (dog) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为:" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
运行结果:不会出现错票、重票
补充:通过实现Runnable接口来解决线程安全问题时,可以考虑使用this来充当同步监视器。
②同步方法:synchronized还可以放在方法声明中,表示整个方法为同步方法。如果被操作的共享数据全都声明在一个方法中,那么不妨将这个方法声明为同步方法。
public synchronized void show (String name){
….
}
以三个窗口卖票问题为例使用同步方法分别解决使用继承Thread类与实现Runnable接口方式的线程不安全问题:
a.使用继承Thread类
class Window4 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while(true){
show();
}
}
private static synchronized void show(){//此时同步监视器是:Window4.class
// private synchronized void show(){//当前同步监视器:this。代表t1、t2、t3,此时是错误的
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出票,剩余:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
运行结果:不会出现错票、重票
b、实现Runnable接口方式
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
public synchronized void show(){//this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为:" + ticket + "张票");
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:不会出现错票、重票
注意:必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就 无法保证共享资源的安全
③锁:Lock——>jdk5.0新增特性。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock类实现了Lock,它拥有synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
} finally{
lock.unlock();
}
}
}
使用Lock的方式解决Runnable实现类的线程安全问题.
class Window implements Runnable{
private int ticket = 100;
//1.创建ReentrantLock类的对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
//调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
finally {
//调用解锁方法unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块。
面试题:synchronized 与Lock的异同?
相同:二者都可以解决线程安全问题。
不同:synchronized机制,在执行完响相应的同步代码后,会自动的释放同步监视器。 Lock锁需要手动的启动同步(lock()),当相应的同步代码执行后,也需要手动的结束(unlock())。
总结:优先使用顺序: Lock——>同步代码块(已经进入了方法体,分配了相应资源)——>同步方法(在方法体之外)
多线程扩展:
a、线程安全的单例模式(懒汉式):要求懒汉式与饿汉式会手写
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式1:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
// }
//方式2:效率稍高
if(instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
b、线程的死锁问题:
死锁 :不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续。
class A {
public synchronized void foo(B b) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
运行中(无法主动结束):
解决方法 ①专门的算法、原则②尽量减少同步资源的定义 ③尽量避免嵌套同步 。