线程的同步:用来解决线程的安全问题
程序中如果写多个线程不一定会出现线程的安全问题,如果存在共享数据,线程才会出现安全问题
例子: 三个窗口卖票,三个窗口的总票数为100张
public void run() {
while(true){
if(tickte>0){
try {
Thread.sleep(100);//如果我们不加这一行,出现票号为0或票号为-1的概率非常小(是可能出现的,因为CPU也可能在这里切换成另一个线程)
} catch (InterruptedException e) {//不能说出现的概率小就不去解决问题
e.printStackTrace();
}
System.out.println(tickte+":"+Thread.currentThread().getName());
tickte--;
}else{
break;
}
}
}
比如现在票还剩一张,线程一先去执行,进入if被阻塞
此时线程二和线程三有非常大的概率也会进来(此时票数是1),也都被阻塞了
最先进入执行状态的线程输出1
在ticket–之后第二个就输出0,最后一个就输出-1。这是出现了错票的安全问题
重票的出现概率比较大的情况:
public void run() {
while(true){
if(tickte>0){
System.out.println(tickte+":"+Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
tickte--;
}else{
break;
}
}
比如某个线程输出了100,然后被阻塞
另外一个线程进来也是100,出现重票
问题分析:
- 问题:卖票的过程中出现重票和错票–>线程安全问题
- 原因:当某个线程操作车票的过程当中,还未操作完成,别的线程就进入处理代码进行操作
- 这个卖票问题的共享数据是ticket
- 解决:当一个线程在操作共享数据时,其他线程不能参与进来,直到这个线程操作完(即使当前线程出现阻塞其他线程也不能进来操作共享数据),其他线程才能操作
- 在java中通过同步机制来解决线程的安全问题
采用实现Runnable接口的解决方法:
方式一:同步代码块
方式二:同步方法
下面的程序演示使用方式一进行解决
同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1.需要被同步的代码:操作共享数据的代码
2.共享数据:多个线程共同操作的变量(数据)
3.同步监视器:俗称:锁,谁拿到锁谁操作代码。任何一个类的对象(包括我们自己定义的类的对象)都能来充当锁。锁不能是基本数据类型
要求:多个线程必须要共用同一把锁,要保证锁的唯一性
总结: 局限性:同步的外面会有并行,但同步的里面只有一个线程执行,所以同步代码块中相当于是单线程,效率低
package test0;
public class ThreadDemo {
public static void main(String[] args) {
MyWindow m=new MyWindow();//因为我们只new过这一个 MyWindow 对象,所以obj只有一个,满足唯一一把锁
Thread t1=new Thread(m);
Thread t2=new Thread(m);
Thread t3=new Thread(m);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
class MyWindow implements Runnable{
Object obj=new Object();
private int tickte =100;//此时不用再加static,三个线程共用ticket(因为是同一个对象给了他们的构造器,除非你再new一个Mywindow,把m1给构造器)
@Override
public void run() {
//Object obj=new Object();不能放在这个地方,这会使得一个线程各自一把锁,不安全。总共三把锁
while(true){
synchronized(obj){//不能写成这样synchronized(new Object()),这样三把锁都不止了
if(tickte>0){
System.out.println(tickte+":"+Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();//即使线程sleep了,别的线程也要等着.直到这个线程操作完成出去了(这个线程还可以回来再抢)
}
tickte--;
}else{
break;
}
}
}
}
}
更简单的方法(针对于实现Runnable接口的方式):使用当前对象来充当锁
package test0;
public class ThreadDemo {
public static void main(String[] args) {
MyWindow m=new MyWindow();
Thread t1=new Thread(m);
Thread t2=new Thread(m);
Thread t3=new Thread(m);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
class MyWindow implements Runnable{
//Object obj=new Object();
private int tickte =100;
@Override
public void run() {
//Object obj=new Object();不能放在这个地方,这会使得一个线程各自一把锁,不安全。总共三把锁
while(true){
synchronized(this){//this是m,调用的run方法是在MyWindow中定义的,MyWindow的对象为this,即m。是唯一的。
if(tickte>0){
System.out.println(tickte+":"+Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();//即使线程sleep了,别的线程也要等着.直到这个线程操作完成出去了(还可以回来再抢)
}
tickte--;
}else{
break;
}
}
}
}
}
继承Thread类的解决方式:
方式一:用同步代码块解决
方式二:同步方法
下面的程序是采用方式一进行解决
package test0;
public class ThreadDemo {
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();
}
}
class window extends Thread{
private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享
private static Object obj=new Object();//不要写成private Object obj=new Object();否则锁不唯一,这个代码有三把锁,因为new了三次
@Override
public void run() {
while (true){
synchronized(obj){
if(ticket>0){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
如果使用继承的方式
不能把obj写this,会让锁不唯一
可以这么干:
把obj改成window.class
- 说明:在继承Thread类创建多线程的方式中,慎用this创建同步监视器(具体问题具体分析,主要看this是否唯一)
- 可以考虑使用当前类来充当同步监视器,反正一定要保证锁唯一
- 同步代码块中不能包含代码多了(效率低,变成单线程。甚至可能会错。),也不能少了,把需要被同步的代码包起来即可
- 包多了可能会错的理解:比如下面把while(true)也包了,那就一个线程把出票这个任务全包了,安全是安全,但实现功能错了
package test0;
public class ThreadDemo {
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();
}
}
class window extends Thread{
private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享
@Override
public void run() {
while (true){
synchronized(window.class){//是用当前类去充当锁,类也是对象。类也是对象会在反射中详细说。类可以作为一个对象出现,是Class的一个对象
if(ticket>0){//类只会加载一次(即window.class只会加载一次),所以window.class是唯一的
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
同步方法(在方法的声明中写上synchronized):
也会涉及到:共享数据和同步监视器的问题
理解:
如果操作共享数据的代码正好完整地声明在一个方法中,我们不妨将此方法声明为同步的。
就叫做同步方法
同理:同步方法中包的东西既不能多,也不能少。所以不能把run变成同步方法(除非run中全是操作共享数据的代码)
实现Runnable接口使用同步方法解决线程安全问题
package test0;
public class ThreadDemo {
public static void main(String[] args) {
MyWindow m=new MyWindow();
Thread t1=new Thread(m);
Thread t2=new Thread(m);
Thread t3=new Thread(m);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
class MyWindow implements Runnable{
private int tickte =100;//此时不用再加static,三个线程共用ticket(因为是同一个对象给了他们的构造器,除非你再new一个Mywindow,把m1给构造器)
@Override
public void run() {
while(true){
show();
}
}
private synchronized void show(){//同步方法的方法体和同步方法块效果是一样的
if(tickte>0){//同步方法中也是有同步监视器的,只是用的是默认的,这个东西是this,又因为本程序中this唯一,所以没问题
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tickte+":"+Thread.currentThread().getName());
tickte--;
}//因为break要用在循环中,就把break干掉了
}
}
//注意:本程序需要手动关闭,可以在while中加条件进行break
使用同步方法解决继承Thread类的线程安全问题
同步方法仍然涉及到同步监视器,只是不需要我们显示声明,用的是默认的
非静态的同步方法的同步监视器是this
静态的同步方法的同步监视器是当前类本身
package test0;
public class ThreadDemo {
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();
}
}
class window extends Thread{
private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享
@Override
public void run() {
while (true){
show();//非静态可以直接调用静态,静态调非静态必须用对象来调用,不能直接调用
if(!(ticket>0)){
break;
}
}
}
private static synchronized void show(){//private synchronized void show()直接这么做不安全,同步监视器为this,以本程序进行分析,同步监视器有三个w1,w2,w3
if(ticket>0){//同步监视器此时不可能是this,因为静态方法不能调this。此时的同步监视器是当前的类window.class
try { //因为当前的类是唯一的,所以锁唯一,安全
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);//写成static后直接写会报错,因为他不是静态的,不能直接调用
ticket--;
}
}
}