先引入线程的生命周期
线程同步问题
再引入问题:模拟火车站售票程序,开启三个窗口售票。
package com.atguigu.java;
/**
* 使用Runnable接口的方式来实现
*/
class window1 implements Runnable{
private int ticket=100; //注意此处没有加static,但三个线程共用同一个ticket
@Override
public void run() {
while(true){
if (ticket>0){
try {
Thread.sleep(100); //此处会发生阻塞,使用sleep()来增加程序出错几率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
ticket--;
}else{
System.out.println("票已经售空");
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
window1 w =new window1(); //只造一个实现类对象
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();
}
}
运行发现:
问题一:(出现重票,线程不安全)
问题二:(出现错票,票号不合法问题。线程不安全)
出现“票号-1”的原因:
问题出现的根源:
当某个线程操作车票(车票为共享数据)的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
实际情境:比如a在厕所一间隔间上厕所,然后a还没有上完厕所时,b就冲了进来,b也要上厕所。那这就很不安全了…
如果我们把门锁上,谁进去谁就拿着这把锁(拿到锁的可以操作),没进去就拿不到这把锁。
所以如何解决:
当一个线程A在操作共享数据时,其他线程不能参与进来,直到线程A操作结束时,其他线程才可以操作。这种情况下,即使线程A出现了阻塞,也不能被改变。
在Java中,我们通过同步机制,来解决线程的安全问题
方式一:同步代码块解决线程安全问题
格式:
synchronized(同步监视器){
//需要被同步的代码
}
如下代码即可解决上述售票程序的两个问题:
package com.atguigu.java;
class window1 implements Runnable{
private int ticket=100;
//造一个对象充当锁
Object obj=new Object();
@Override
public void run() {
while(true) {
synchronized (obj) {
if (ticket > 0) { //把操作共享数据的代码包起来
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票成功售出,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已经售空");
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
window1 w =new window1(); //只造一个实现类对象
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();
}
}
实际上不需要new一个Object对象,在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
package com.atguigu.java;
class window1 implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true) {
synchronized (this) { //传入this可行。此处this表示唯一的window1的对象w
if (ticket > 0) { //把操作共享数据的代码包起来
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票成功售出,票号为:" + ticket);
ticket--;
} else {
System.out.println("票已经售空");
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
window1 w =new window1(); //只造一个实现类对象
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();
}
}
说明:
1,操作共享数据的代码,即为需要被同步的代码。
2,共享数据即多个线程共同操作的变量,如ticket
3,同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。但要求多个线程必须要共用同一把锁。
优点: 解决线程的安全问题。
缺点: 操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低。
练习:
使用同步代码块方式处理继承Thread类的线程安全问题
package com.atguigu.java;
/**
* 例子:创建三个窗口买票,总票数为100张
* 使用同步代码块解决继承Thread类的方式的线程安全问题
*/
public class WindowTest {
public static void main(String[] args) {
Window t1 =new Window();
Window t2 =new Window();
Window t3 =new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window extends Thread{
static Object obj=new Object(); //注意此处必须加static来保证三个window对象共用一个obj
private static int ticket=100; //static。三个窗口共享同一个ticket
@Override
public void run() {
while(true){
//正确的
synchronized (obj){
//而此处用synchronized(this){ 是错误的。因为此处的this代表着t1,t2,t3三个对象,对象(锁)不唯一,线程不安全
if (ticket>0){
try {
Thread.sleep(100); //增大线程不安全现象出现的几率(使程序阻塞一段时间)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+":票成功售出,票号为:"+ticket);
ticket--;
}else{
System.out.println("票已经售空");
break;
}
}
}
}
}
方式二:同步方法解决线程安全问题
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
①使用同步方法解决实现Runnable接口的线程安全问题
package com.atguigu.java;
/*
* 使用同步方法解决实现Runnable接口的线程安全问题
*/
class window3 implements Runnable{
private int ticket=100; //注意此处没有加static,但三个线程共用同一个ticket,因为共用同一个window3对象
@Override
public void run() {
while(true){
show(); //调用show方法
}
}
//定义一个show方法
private synchronized void show(){ //声明为同步方法。有同步监视器,此处默认为this
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
window3 w =new window3(); //只造一个实现类对象
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();
}
}
②使用同步方法解决继承Thread类方式中的线程安全问题
package com.atguigu.java;
/**
* 例子:创建三个窗口买票,总票数为100张
* 使用同步方法解决继承Thread类方式中的线程安全问题
*/
public class WindowTest4 {
public static void main(String[] args) {
Window t1 =new Window();
Window t2 =new Window();
Window t3 =new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window extends Thread{
private static int ticket=100; //static。三个窗口共享同一个ticket
@Override
public void run() {
while(true){
show(); //非静态方法也可以调用静态结构
}
}
private static synchronized void show(){//若想要用同步方法解决线程不安全需要将方法设为静态,此时的同步监视器(静态同步方法为当前类:window.class)才唯一
//private synchronized void show(){ //此方法不正确。同步监视器this指的是:t1,t2,t3。
if (ticket>0){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//静态方法只能调用静态结构。通过对象调用线程名保证不报错(Thread.currentThread().getName())
System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
ticket--;
}
}
}
关于同步方法的总结:
①同步方法仍然涉及到同步监视器,只是不需要我们显式地声明;
② 对于非静态的同步方法,同步监视器是this;
对于静态的同步方法,同步监视器是当前类本身
解决懒汉式单例模式的线程安全问题
使用同步方法改造懒汉式单例模式
package com.atguigu.java1;
//使用同步方法将单例模式中的懒汉式实现修改为线程安全
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance=null; //instance当作共享数据
public static synchronized Bank getInstance(){ //对于静态的同步方法,同步监视器是当前类本身。此处为Bank.class
if (instance==null){ //如果不同步,此处可能会出现阻塞,有线程安全问题
instance=new Bank();
}
return instance;
}
}
使用同步代码块改造懒汉式单例模式
情形一代码:(效率稍差)
package com.atguigu.java1;
/**
* 使用同步代码块将单例模式中的懒汉式实现修改为线程安全的
*/
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance=null; //instance当作共享数据
public static Bank getInstance(){
synchronized (Bank.class) {
if (instance==null){ //如果不同步,此处可能会出现阻塞,有线程安全问题
instance=new Bank();
}
return instance;
}
}
}
上述情形一代码效率稍差。当多个线程走到synchronized结构时,线程一拿到锁之后进入此结构,造一个对象,然后return;后面的线程进来后判断发现,对象不为空,就直接把线程一造好的对象进行return。
就像鸡排店剩下今天最后一份大脸鸡排,后面队伍还很长,第一个人进去买完带着鸡排回去了,后面的人都继续排队询问还有没有鸡排。
这显然效率不高,所以老板可以直接贴出公告说明已经卖完,后面的不用再等了。
情形二代码:(效率稍高,面试时写这个)
package com.atguigu.java1;
/**
* 使用同步机制将单例模式中的懒汉式实现修改为线程安全的
*/
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance=null;
public static Bank getInstance(){
//效率稍高
if (instance==null){
synchronized (Bank.class) { //里面认为是在操作共享数据
if (instance==null){
instance=new Bank();
}
}
}
return instance;
}
}
死锁问题
我们使用同步时,要避免出现死锁。
比如做了一桌饭,两个人吃,只有一双筷子。如果一个人先拿到这双筷子就可以先吃,吃完再让另一个人吃;但是如果一人拿了一只筷子,都在等对方放弃自己的筷子,僵持不下,程序就进入了死锁状态。
package com.atguigu.java1;
/**
* 演示线程的死锁问题
*/
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1=new StringBuffer();
StringBuffer s2=new StringBuffer();
new Thread(){ //匿名创建线程,继承Thread类方式
@Override
public void run() {
synchronized (s1){
s1.append("a"); //给字符串添加一个a
s2.append("1");
try {
Thread.sleep(100); //增加死锁现象出现的概率,注意sleep()不会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){ //嵌套
s1.append("b");
s2.append("1");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//实现Runnable接口方式
new Thread(new Runnable() { //传入一个匿名的Runnable接口实现类的对象
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100); //增加死锁现象出现的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){ //嵌套
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
第一个线程握住锁s1,调用append()添加字符,然后执行到sleep()时会进入阻塞状态。此时第二个线程就可能会执行,握住锁s2,调用append()添加字符,也然后执行到sleep()。当二者sleep结束时,第一个线程拿着s1等着拿s2,第二个线程拿着s2等着拿s1。二者就会“僵持不下”。
程序不抛异常,也不终止----->死锁
死锁演示:
package com.atguigu.java1;
//死锁演示
class A {
public synchronized void foo(B b) { //foo方法为同步方法。锁(this)为A的对象a
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() { //同步监视器(this):A类的对象a
System.out.println("进入了A类的last方法内部"); //同步监视器:A类的对象a
}
}
class B {
public synchronized void bar(A a) { //同步监视器(this)为b
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(); //所以主线程要先握a后握b,此方法才能结束,接下来才能释放b和a
}
public synchronized void last() { //同步监视器(this)为b对象
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(); //主线程调用init()方法
}
}
①主线程要先握a后握b,foo方法才能结束,接下来才能释放b和a。
②分线程要先握b后握a,bar方法才能结束,接下来才能释放b和a。
会出现死锁问题。
方式三:Lock锁解决线程安全问题
格式:
仍以“三个窗口售票100张”为例感受Lock锁用法:
package com.atguigu.java1;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock锁解决线程安全问题----jdk5.0新增。Lock本身是一个接口,具体使用的是它的实现类ReentrantLock
*/
class Window0 implements Runnable{ //注意此处为实现Runnable接口方式下使用Lock锁。继承Thread方式需要使用static修饰实现类对象
private int ticket=100;
//①创建一个ReentrantLock对象(实例化ReentrantLock)
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true){
try { //要操作的代码放到try中
//②调用锁定方法lock()。表示在此过程中它是一个单线程的,
lock.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":成功售出,票号为:"+ticket);
ticket--;
}else{
System.out.println("票卖完了");
break;
}
}finally{ //一定会执行
lock.unlock(); //③调用解锁方法unlock()。
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window0 w=new Window0();
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();
}
}
常问的一个面试题:
synchronized 与 Lock 的异同?
同:二者都可以解决线程安全问题。
异:synchronized 机制执行完相应的同步代码之后自动释放同步监视器。Lock 需要手动去启动同步(lock()方法),手动结束同步(unlock()方法),更灵活。
练习:
分析:
- ①两个储户涉及两个线程;
- ②存在共享数据,账户(余额)。可能存在线程安全问题(都操作了共享数据);
我们有三种方式解决线程安全问题。
我们选用同步方法方式:
package com.atguigu.java1;
/**
* 银行有一个账户。
* 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打
* 印账户余额。
*/
class Account{
private double balance;
//构造器
public Account(double balance) {
this.balance = balance;
}
//存钱方法设为同步方法。一个人存完之后,另一个人才能存
public synchronized void deposit(double amt){ //此处的锁(this)为acct,唯一。
if (amt>0){
balance+=amt;
try {
Thread.sleep(1000); //增大线程不安全问题出现概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功。余额为:"+balance); //此处有线程安全问题
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct) { //alt+insert快捷键选择构造器
this.acct = acct;
}
//重写run()
@Override
public void run() { //要执行的操作是存钱
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) { //这里面需要new两个储户
Account acct=new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct); //此时两个对象共用同一个账户
//给线程起名字
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}