线程的生命周期
1.源码中定义的生命周期
源码中的状态定义的界限不是很清楚,那么通常一个线程的完整生命周期需要经历五种状态。
1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程处于新建状态
2.就绪:处于新建状态的线程被start()后,将进入线程列表等待cpu的时间片,此时它已经有了运行的条件,只是还没有得到cpu的资源。
3.运行:当就绪的线程被调度并获取cpu执行权时,便进入了运行的状态,run()方法中就定义了线程要完成的操作和功能。
4.阻塞:在某种特殊的情况下,被认为的挂起或是等待程序的输入输出,让出cpu并且临时终止自己的执行,进入阻塞状态
5.死亡:线程完成了它的全部工作、线程被提前强制终止或者线程发送了错误或异常并且未处理异常
线程的同步
问:为什么需要线程同步或者为什么需要线程安全呢?
场景:利用多线程卖票窗口
class Window2Test implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票,号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class Window2 {
public static void main(String[] args) {
Window2Test test = new Window2Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
t1.setName("1");
t2.setName("2");
t3.setName("3");
t1.start();
t2.start();
t3.start();
}
}
执行结果:
可以看到以上两种情况一个是错票,一个是重票,这些都是不合法的结果
那么造成这样结果的原因是什么呢?
多条语句在操作同一线程的共享数据时,一条线程对多条语句还未执行完,另外一条线程也参与进来,导致了共享数据的错误。
解决方法:
对于多条操作共享数据的语句,当一条线程正在执行时,其他线程不能参与进来,能等待该线程执行完毕,其他线程才能参与
1.方式一:使用同步代码块
synchronized(同步监视器){
//需要同步的代码
}
说明:
①同步监视器:俗称:锁,任何一个类的对象,都可以充当同步监视器,要求多个线程必须共用一个同步监视器。
②同步代码:多个线程共同操作的变量,例如卖票时的票数
③同步代码:操作共享数据时的代码
2.方式二:同步方法,在方法上加上synchronized的修饰
3.方式三:Lock() 锁—JDK5.0新增
线程安全的实例
1.在继承Thread类创建线程的方式中使用同步代码块
public class Window1 {
public static void main(String[] args) {
WindowTest1 window1 = new WindowTest1();
WindowTest1 window2 = new WindowTest1();
WindowTest1 window3 = new WindowTest1();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class WindowTest1 extends Thread{
private static int ticket = 100; //方式一:使用static 控制同一份买票数量
private static Object obj = new Object();
@Override
public void run() {
//错误
//synchronized(this) {
//正确
// synchronized(WindowTest1.class){
//正确
synchronized(obj){
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":买票,号码:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
说明:
在Window1类中我们声明了三个Thread子类对象,也就是创建了三个新线程。因此需要对Thread子类的run方法做加锁处理。
使用同步代码块的方式,我们需要在synchronized中传入一个对象
synchronized(obj){
//。。。
}
这个对象可以是任何类的对象,那么有一个要求就是传入的对象对于三个线程来说是同一个,为了实现这个要求,我们可以将Object类声明为static,那么对于三个线程来说就是同一个类对象了,
此时就达到了我们要求的同步代码的效果。
那么,除了传入一个类的对象,还有没有别的方式呢?这里我给出了另外一种方式,就是传入WindowTest1.class,传入一个类,类也是对象所以可以传入当前类本身。
2.在实现Runnable接口方式中使用同步代码块
class Window2Test implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true){
synchronized(this){ //this当前对象 // synchronized(obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":买票,号:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class Window2 {
public static void main(String[] args) {
Window2Test test = new Window2Test();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
t1.setName("1");
t2.setName("2");
t3.setName("3");
t1.start();
t2.start();
t3.start();
}
}
同样这里也要保证同步监听器的要求,传入的对象必须是线程所共用的。
Window2Test 子类并没有在main方法中被实例多次,因此,我们可以在Window2Test中实例一个Object类对象,传入同步代码块的参数中,作为监视器。
另外,还可以传入this,这里this指代的就是当前类的对象,当然也可以传入当前类Window2Test.class
3.在继承Thread类创建线程的方式中使用同步方法
public class Window4 {
public static void main(String[] args) {
WindowTest4 window1 = new WindowTest4();
WindowTest4 window2 = new WindowTest4();
WindowTest4 window3 = new WindowTest4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class WindowTest4 extends Thread{
private static int ticket = 100; //方式一:使用static 控制同一份买票数量
@Override
public void run() {
while (true){
show();
}
}
//private synchronized void show(){ 错误 当前锁对象是 window1 window2 window3
private synchronized static void show(){ //此时锁 对象是WindowTest4.class
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,号码:"+ticket);
ticket--;
}
}
}
这里唯一需要注意的一点就是synchronized修饰的方法必须要声明整静态,因为,这里虽然没有要求我们传入同步监视器,实际上是Java帮我们加上的,属于隐式写法。这时它传入的其实就是类本身。
4.在实现Runnable接口方式中使用同步方法
class Window2Test3 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
public synchronized void show(){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,号:"+ticket);
ticket--;
}
}
}
public class Window3 {
public static void main(String[] args) {
Window2Test3 window2Test = new Window2Test3();
Thread thread1 = new Thread(window2Test);
Thread thread2 = new Thread(window2Test);
Thread thread3 = new Thread(window2Test);
thread1.setName("1");
thread2.setName("2");
thread3.setName("3");
thread1.start();
thread2.start();
thread3.start();
}
}
这种方式使用同步方法就简单了,其实它隐式传入的同步监视器就是this了
5.在继承Thread的方式中使用Lock()
我们使用Lock()这种方式时,其实使用的时它的子类ReentrantLock
public class LockTest2 {
public static void main(String[] args) {
Window2 window1 = new Window2();
Window2 window2 = new Window2();
Window2 window3 = new Window2();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class Window2 extends Thread{
private static int ticket = 100;
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
//加锁
lock.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":售票:"+ticket);
ticket--;
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
这里代码也比较简单,这种方式在于手动加锁和解锁。
这就是三种线程同步的方式,也可以说是两种,synchronized 和Lock()
那么这两种有什么对比
1.Lock时显示锁(手动开启和关闭锁),synchronized时隐式锁,出了作用域自动释放
2.Lock只有代码块锁,synchronized有代码块和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock→同步代码块(已经进入了方法体,分配了相应资源)→同步方法(在方法体之外)
线程死锁问题
死锁:在不同的线程分别占用对方需要的同步资源不放弃,都在等待对方先放弃自己需要的同步资源,就形成了线程的死锁问题。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
1.专门的算法、原则
2.尽量减少同步资源的定义
3.尽量避免嵌套同步
死锁举例
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();
}
}