【Java进阶篇】第七章 多线程
创始人
2024-03-23 03:21:27
0

文章目录

  • 一、多线程概述
    • 1、进程与线程
    • 2、进程与线程的关系
  • 二、多线程并发的实现
    • 1、线程的实现方式一
    • 2、线程的实现方式二
  • 三、线程的生命周期
    • 1、线程的五个生命周期
    • 2、常用方法
    • 3、线程的sleep
    • 4、终止线程的睡眠状态
    • 5、强行终止线程的执行
    • 6、合理终止一个线程的执行
  • 四、线程的调度
    • 1、线程调度的模型
    • 2、线程调度的方法---优先级
    • 3、线程调度---让位
    • 4、线程调度--线程合并
  • 五、线程安全
    • 1、同步与异步
    • 2、同步机制synchronized
    • 3、有线程安全的变量
    • 4、synchronized出现在实例方法上
    • 5、synchronized的三种写法
  • 六、死锁
    • 1、原理
    • 2、代码实现
  • 七、线程守护
    • 1、线程守护的概述
    • 2、实现守护线程
    • 3、定时器
    • 4、实现定时器
    • 5、实现线程的第三种方式
  • 八、wait和notify
    • 1、概述
    • 2、生产者和消费者模式

一、多线程概述

1、进程与线程

  • 进程是一个应用程序(一个进程是一个软件)
  • 线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程

举个例子:

DOS窗口运行java HelloWorld,先启动JVM,JVM是一个进程,JVM启动一个主线程调用main方法,同时再启动一个垃圾回收线程来负责看护、回收垃圾。(也就是说Java程序至少两线程并发,main方法对应的主线程+GC)

2、进程与线程的关系

把进程看作是现实生活中的公司,如京东。线程则可看作是其下的某一个职能部门,负责完成某任务,如开发部门。

  • 进程A和进程B的内存独立不共享
  • Java中,线程A和线程B,堆内存和方法区内存共享,但栈内存独立,一个线程一个栈

如启动了10个线程,就会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

🍁Java中的多线程机制,目的就是为了提高程序的处理效率, 如火车站看成是一个进程,则每个售票小窗口就是一个个线程,甲在窗口1买票,乙在窗口2买票,谁也不用等谁 一个个售票窗口就像一个个栈,有自己独立的空间。售票大厅这个共用空间就像堆和方法区。

多线程
引入多线程以后,main方法的结束,不再意味着程序的结束。main方法结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈

二、多线程并发的实现

真正的多线程并发是:t1线程执行t1,t2线程执行t2,t1不影响t2,t2不影响t1。4核的CPU,在同一时间点,可以真正的有4个进程并发执行,单核的CPU,在某一个时间点上实际只能处理一件事情,但由于CPU的处理速度极快,多个线程之间频繁切换,从而造成了多个事情在同时处理的视觉假象。

public class Thread1 {public static void main(String[] args) {m1();    }public static void m1(){m2();    }public static void m2(){m3();}public static void m3(){System.out.println("m3 excute");}}

分析以上:只有一个主线程,主栈,没有分支线程被启动
示意图

1、线程的实现方式一

编写一个类,直接继承java.lang.Thread,重写run方法

class MyThread extends Thread{//这段代码运行在分支线程中public void run(){for(int i=0;i<100;i++){System.out.println("分支线程" + i);}}
}
public class ThreadTest{public static void main(String[] args) {//创建分支线程对象MyThread myThread = new MyThread();//启动分支线程myThread.start();//这里仍然运行在主线程当中for(int i=0;i<100;i++){System.out.println("主线程" + i);}}
}

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这个任务完成后,这段代码就瞬间执行结束了

启动成功后的线程会自动调用我重写的run方法,并且run方法在分支栈的栈底部(main方法在主栈的底部,故run和main是平级的)

myThread.run();

如果直接调用我重写的run方法,则不会启动线程,不会分配新的分支栈,此时,就是单线程了。
内存图
运行结果:
run

2、线程的实现方式二

编写一个类,实现java.lang.Runable接口,重写run方法

public class ThreadTest {public static void main(String[] args) {//创建一个可运行的对象MyRunnable r = new MyRunnable();//给Thread类的构造方法传入Runnable类的对象//将可运行的对象封装成一个线程对象Thread t = new Thread(r);t.start();for(int i=0;i<100;i++){System.out.println("主线程"+ i);}}
}//这不是线程,仅仅是一个可运行的类
class MyRunnable implements Runnable{public void run(){for(int i=0;i<100;i++){System.out.println("分支线程"+i);}}
}

总结线程的实现:

🍁编写一个类继承Thread类并重写run方法

public class MyThread extends Thread{public void run(){}
}MyThread t = new MyThread();
t.start();

🍁编写一个类,实现Runnable接口并重写run方法

public class MyRunnbale implements Runnable{public void run(){}
}Thread t = new Thread(new MyRunnable());
t.start();

由于Java的单继承,第一种方式中,不能再继承别的类了,而第二种可以,面向接口编程更优。

//方式二的匿名内部类写法:Thread t2 = new Thread(new MyRunnable(){public void run(){for(int i=0;i<100;i++){System.out.println(i);}}});

tip

run()方法中要是有异常,也只能捕捉,不能上抛,因为run方法在父类中没有抛出任何异常,做为子类,重写时不能比父类抛出更多的异常。

三、线程的生命周期

1、线程的五个生命周期

生命周期图

  • 🍁 新建状态:
    刚new出来的线程对象

  • 🍁就绪状态:
    又叫做可运行状态,表示当前线程具有抢夺CPU时间片的能力(CPU时间片就是执行权)当一个线程抢夺到CPU时间片后,开始执行run方法,run方法的执行标志着线程进入运行状态。

  • 🍁运行状态:
    run方法开始执行,线程进入运行状态,当之前占有的CPU时间片用完之后,重新回到就绪状态继续抢夺CPU时间片,待抢到后,重新进入run方法上次执行的地方继续执行

  • 🍁死亡状态:
    run方法执行结束,线程到达死亡状态

  • 🍁阻塞状态:
    当一个线程遇到阻塞事件,如接收用户键盘输入,sleep方法,则进入阻塞状态,此时线程会放弃之前抢占到的CPU时间片

2、常用方法

获取线程的名字getName()

MyThread myThread = new MyThread();
String tName = myThread.getName();
//Thread-0
System.out.println(tName);
//更改
myThread.setName("code-9527 's Thread");
System.out.println(myThread.getName());

获取当前线程对象currentThread()

//静态方法获取当前线程对象,返回一个Thread类型对象
Thread currentThread = Thread.currentThread();
//返回线程名
System.out.println(currentThread.getName());

3、线程的sleep

//毫秒
static void sleep(Long millis);

作用是让当前线程进入休眠,进入阻塞状态,放弃占有CPU时间片,让给其他线程去使用,出现的地方,对应的线程休眠

public static void main(String[] args) {try{Thread.sleep(1000*5); //让当前线程(main)休眠五秒}catch(InterruptedException e){e.printStackTrace();}//五秒后被输出System.out.println("sleep结束");
}

实现间隔特定的时间去执行一段特定的代码

sleep是静态方法,若上面的Thread.sleep改成:

Thread t = new MyThread();
...
t.sleep(1000*5);

执行的时候,t.sleep(1000*5);还是会被当作Thread.sleep(1000*5);,被休眠的也还是main线程,而不是t线程。

4、终止线程的睡眠状态

t.interrupt();

干扰,即中断t线程的睡眠,执行后sleep()出现异常,即catch(InterruptedException e),这种中断睡眠状态的方式,依靠的时Java的异常处理机制。

class MyThread extends Thread{public void run(){try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}for(int i=0;i<100;i++){System.out.println("分支线程" + i);}}
}
class ThreadTest1{public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start();//线程myThread计划sleep5秒//扔出interrupt后就提前醒来了myThread.interrupt();}
}

示意图

5、强行终止线程的执行

线程对象的引用.stop()

stop方法已过时,容易丢数据。这种方式是直接将线程杀死了,线程没有保存的数据会丢失

MyThread myThread = new MyThread();
myThread.start();
//终止
myThread.stop();

强行终止

6、合理终止一个线程的执行

  • 编写的类中加入run属性:boolean run = true;
  • 重写run方法的时候,if(run)…
  • else中写终止线程之前要保存的数据和操作+return;
  • 以后则只需改线程对象的run属性即可终止线程
class MyRun implements Runnable{boolean run = true;public void run() {for (int i = 0; i < 100; i++) {if (this.run) {System.out.println(Thread.currentThread().getName() + "--->" + i);System.out.println(this.run);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}} else {System.out.println("这是一些终止线程前要做的事");System.out.println("保存数据中..终止线程成功!");return;}}}
}
public class ThreadTest2 {public static void main(String[] args) {MyRun r = new MyRun();Thread t = new Thread(r);t.start();//sleep主线程三秒try{Thread.sleep(3000);}catch(InterruptedException e){e.printStackTrace();}//终止,改run属性为falser.run = false;}
}

运行效果:
run

四、线程的调度

1、线程调度的模型

  • 🍁抢占式调度模型:
    哪个线程的优先级比较高,抢到CPU时间片的概率就高一些/多一些。Java中采用的就是抢占式调度模型。

  • 🍁均分式调度模型:
    平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样

2、线程调度的方法—优先级

  • 设置线程的优先级
void setPriority(int newPriority)
  • 获取线程的优先级
int getPriority()
//最低优先级为1
static int MIN_PRIORITY
//默认优先级为5
static int NORM_PRIORITY
//最高优先级为10
static int MAX_PRIORITYSystem.out.println(Thread.MAX_PRIORITY);

举例:

Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName()+"的优先级是:"+ currentThread.getPriority());currentThread.setPriority(9);

3、线程调度—让位

//静态方法

static void yield()

暂停当前正在执行的线程对象,去执行其他线程,yield方法的执行会让当前线程从运行状态进入就绪状态 ,注意不是阻塞状态。

class MyRunnable2{public void run(){for(int i=0;i<101;i++){//每循环10次,让当前线程暂停让位一下if(i%10 == 0){Thread.yield();}System.out.println(Thread.currentThread().getName() + i);}}
}

4、线程调度–线程合并

实例方法void join(),注意线程合并不是栈的合并

MyThread t = new MyThread();
//让当前线程阻塞,t线程执行,直到t线程执行结束,当前线程才执行
t.join();

五、线程安全

线程安全问题的产生条件:

  • 多线程并发
  • 有贡献数据
  • 共享数据有修改行为

线程安全
线程安全问题的解决–线程同步机制

线程同步即线程排队执行,不能并发了(可能会牺牲一部分效率,但数据安全是一切的前提)

1、同步与异步

  • 🍁异步模型:
    异步就是并发,线程t1和线程t2各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,即多线程并发,效率较高。

  • 🍁同步模型:
    线程t1和t2,t1执行的时候,必须等待t2执行结束,效率较低。

❀❀❀账户安全问题的代码模拟:


/*** 账户类*/
public class Account {private String actno;private double balance;public Account(){}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/*** 取款方法* @param money*/public void withdraw(double money){double before = this.getBalance();double after = before - money;//别立即更新余额,使用休眠模拟网络延迟try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}this.setBalance(after);}
}
public class AccountThread extends Thread{//该类的对象有Account属性//某人“有一个账户”private Account account;//通过构造方法传递账户对象public AccountThread(Account account){this.account = account;}public void run(){double money = 5000;account.withdraw(money);System.out.println(Thread.currentThread().getName() + "对账户:" + account.getActno() +"取款:" + money + ",余额:"+ account.getBalance());}
}class Test{public static void main(String[] args) {Account account = new Account("act-001",10000);//两个线程共用一个账户对象AccountThread t1 = new AccountThread(account);AccountThread t2 = new AccountThread(account);t1.setName("t1");t2.setName("t2");t1.start();t2.start();}
}

运行结果:

run

2、同步机制synchronized

🍁语法:

synchronized(){

线程同步代码块

}

小括号中传的是多个线程共享的对象,若有t1、t2、t3、t4、t5线程,t1、t2、t3需要排队,t4、t5不用,则()中是一个t1、t2、t3共享的对象,而这个对象t4、t5不共享

由此,上面例题中的取款方法变为:

public void withdraw(double money){synchronized(this) {double before = this.getBalance();double after = before - money;try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}}

简单的说就是synchronized内部放的是要排队执行的代码块

🍁对象锁:

在Java中,任何一个对象都有“一把锁”,这把锁其本质是一个标记,一个对象一把锁。

当运行状态的线程遇到synchronized关键字:

  • 在锁池lockpool中找共享对象的对象锁。线程进入锁池找共享对象的对象锁之前,会释放之前占有的CPU时间片
  • 若找到了,则进入就绪状态继续抢夺CPU时间片,若没找到,则在锁池中等待

🍺例:
当t1和t2线程并发:
t1先遇到synchronized,自动找所共享对象的对象锁,找到之后,占有这把锁,然后执行同步代码块中的代码, 直到同步代码块执行结束,这个锁才释放

—>>>

t1占有对象锁后,t2线程若也遇到了synchronized,在找对象锁时,发现被t1占有,则t2在同步代码块外等待t1结束并释放对象锁后,再占有对象锁、执行同步代码块

3、有线程安全的变量

存在于堆区中的实例变量、存在于方法区中的静态变量,因为堆和方法区均只有一个,且是多线程共享的,有可能存在安全问题。

局部变量存在于栈区中,永远不会有线程安全问题,因为一个线程一个栈。

4、synchronized出现在实例方法上

旧版
改为:

public synchronized void withdraw(double money){double before = this.getBalance();double after = before - money;try{Thread.sleep(1000*5);}catch(InterruptedException e){e.printStackTrace();}this.setBalance(after);}

这样写的缺点:

  • synchronized出现在实例方法上,表示整个方法都需要同步(实际只有其中一部分代码需要同步),这样就扩大了同步的范围,导致程序的执行效率变低
  • synchronized出现在实例方法上,锁的就一定是this,就不能是其他对象了

5、synchronized的三种写法

🍁

synchronized(线程共享对象){同步代码块;
}

🍁
在实例方法中使用synchronized,表示共享的对象一定是this,并且同步的代码块是整个方法体

🍁
在静态方法中使用synchronized,表示找类锁,类锁永远只有1把(对象锁是100个对象就有100个对象锁)

六、死锁

1、原理

死锁示意图
t1线程执行某同步代码块,用到了对象1和2,即t1线程需要先锁对象1,再锁对象2,全锁以后,算同步代码块执行结束,然后一下释放两个对象锁

t2线程执行另一个同步代码块,需要先锁对象2,再锁对象1才算这个同步代码块执行结束,然后释放两个对象锁。

如此:
t1锁到对象2的时候,发现已被锁,则等待,而另一边:t2锁到对象1的时候,发现对象1已被锁,两个线程同时陷入无休止的等待…尬住了

2、代码实现

class MyThread1 extends Thread{Object o1;Object o2;public MyThread1(Object o1, Object o2){this.o1 = o1;this.o2 = o2;}public void run(){//同步代码块开始synchronized(o1){try{//别着急锁o2,为了保证死锁必现,这里等两秒Thread.sleep(2000);}catch(InterruptedException e){e.printStackTrace();}//synchronized的嵌套//从而实现:对象o1和o2都锁了才算同步代码块结束synchronized(o2){}}//同步代码块结束}
}class MyThread2 extends Thread{Object o1;Object o2;public MyThread2(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o2){try{Thread.sleep(2000);}catch(InterruptedException e){e.printStackTrace();}synchronized(o1){}}}
}public class DeadLock{public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();//启动两个线程,共用对象o1和o2MyThread1 t1 = new MyThread1(o1,o2);MyThread2 t2 = new MyThread2(o1,o2);t1.start();t2.start();}
}

运行结果:

run
死锁发生后,程序不出异常,也不报错,会一直僵持着,不易发现并调试。

tips

关于synchronized死锁和线程安全的优化:

synchronized会让程序执行效率变低,系统吞吐量降低,用户体验变差。解决线程安全,可考虑:

  • 使用局部变量代替实例变量和静态变量
  • 若必须使用实例变量,考虑多创建几个对象,别对象共享了也就没有安全问题了
  • 若以上两条都做不到,则用synchronized

七、线程守护

1、线程守护的概述

Java中,线程分为两大类:

  • 用户线程,如主线程main线程
  • 守护线程,如经典的垃圾回收线程

守护线程的特点是:

一般守护线程是一个死循环,所有用户线程结束的时候,守护线程自动结束

2、实现守护线程

通过实例方法setDaemon:

public class DaemonTest {public static void main(String[] args) {Thread t = new BackupThread();t.setName("备份守护线程");//传入true,则普通线程变守护线程t.setDaemon(true);t.start();}}
class BackupThread extends Thread{public void run(){//要进行的守护动作写在run方法中即可int i = 0;while(true){//即使是死循环,但做为守护线程,当所有线程都结束的时候,也会自动结束}}
}

3、定时器

🍁作用:
间隔特定的时间,执行特定的程序

🍁应用场景:
如每周进行银行账户的总账操作,每天进行数据库的备份操作

🍁实现方式:

  • 用sleep方法,设置线程睡眠,睡到某时刻醒来执行代码完成任务
  • 用java.util.Timer
  • 用Spring框架中的SpringTask框架(底层还是java.util.Timer)

4、实现定时器

用到的java.util.Timer类中的方法

  • Time类的无参构造方法,创建定时器对象
Timer timer = new Timer();
  • Timer类的有参构造
//传入true,表示以守护线程的方式
Timer timer = new Timer(true);
  • schedule方法
/**安排任务从指定时间开始进行重复固定的延迟执行
* TimerTask是一个抽象类
* Date firstTime即第一次执行的时间
* Long period 即间隔多少毫秒
*/
void schedule(TimerTask task, Date firstTime , Long period)//安排任务在指定时间执行任务task
void schedule(TimerTask task, Date time)

❀代码实现–编写一个定时器任务类记录日志

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;public class LogTimerTask extends TimerTask {/*** 重写父类TimerTask中的抽象方法run* TimerTask类实现了Runnable接口,所以有run方法*/public void run(){//这里写要执行的任务SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String strTime = sdf.format(new Date());System.out.println(strTime + "日志记录成功");}
}class TimerTest{public static void main(String[] args) {Timer timer = new Timer();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");try {Date firstTime = sdf.parse("2022-12-06 08:10:06");timer.schedule(new LogTimerTask(),firstTime,1000*5);} catch (ParseException e) {e.printStackTrace();}}
}

运行效果:
run

5、实现线程的第三种方式

JDK8新特性,可实现Callable接口。这种方式实现的线程可以获得线程的返回值。前两种实现方式不能获取返回值,因为run方法的返回值类型是void。

优点:
可获取到线程的执行结果

缺点:
效率较低,在获取t线程执行的结果时,当前线程需要等待,等拿到返回值以后再往下执行其余code

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadTest0 {public static void main(String[] args) {//创建一个”未来任务类“的对象,传参为Callable接口实现类的对象FutureTask task = new FutureTask(new Callable(){  //匿名内部类//call方法相当于run方法,不过其有返回值public Object call() throws Exception{System.out.println("call method begin!");Thread.sleep(1000*6);System.out.println("call method end");int a = 100;int b = 200;//线程执行一个任务,执行完可能有返回结果//这里自动装箱return a+b;}});Thread t = new Thread(task);t.start();try {//当前为主线程,获取t线程的执行结果Object obj = task.get();System.out.println("线程执行结果:" + obj);//此处get方法要拿另一个线程的执行结果,可能要很久//但这导致了下面main线程要等待get执行结束。//这就导致了”当前线程的阻塞“System.out.println("这里要等get拿到线程的返回值才能执行");} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

运行结果:
run

八、wait和notify

1、概述

wait 和notify方法是Java中任何一个Java对象都有的方法,因为这两个方法是Object类自带的。不是给线程对象用的,所以别t.wait();

Object o = new Object();
o.wait();

以上让正在o对象上活动的线程进入等待状态,且为无限等待,直到被唤醒为止

示意图

T线程在o对象上活动,o.wait()后,T线程进入无限期等待,并且释放o对象的对象锁

o.notify()让正在o对象上等待的线程被唤醒

notifyAll()方法是唤醒o对象上处于等待的所有线程


2、生产者和消费者模式

生产者-消费者

🍁代码实现:

import java.util.ArrayList;
import java.util.List;/*** 生产线程*/
class Producer implements Runnable{/*** 仓库*/private List list;public Producer(List list){this.list = list;}public void run(){while(true){synchronized(list){if(list.size()>0){ //仓库有东西,则生产线程waittry {list.wait(); //进入等待状态,释放之前占有的list的对象锁} catch (InterruptedException e) {e.printStackTrace();}}//能到这说明仓库空了,开始生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "-->" + obj);list.notifyAll(); //生产完了以后唤醒消费线程来消费}}}
}/*** 消费线程*/
class Consumer implements Runnable{private List list;public Consumer(List list){this.list = list;}public void run(){while(true){synchronized(list){if(list.size() == 0){ //仓库已空,暂停消费try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//消费Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "-->" + obj);//唤醒生产线程来生产list.notifyAll();}}}
}/*** 测试*/
public class PC_Moudle {public static void main(String[] args) {//创建一个仓库,生产和消费线程共享List list = new ArrayList();Thread t1 = new Thread(new Producer(list));Thread t2 = new Thread(new Consumer(list));t1.setName("生产者线程");t2.setName("消费者线程");t1.start();t2.start(); }
}

运行效果:
run

相关内容

热门资讯

汽车油箱结构是什么(汽车油箱结... 本篇文章极速百科给大家谈谈汽车油箱结构是什么,以及汽车油箱结构原理图解对应的知识点,希望对各位有所帮...
美国2年期国债收益率上涨15个... 原标题:美国2年期国债收益率上涨15个基点 美国2年期国债收益率上涨15个基...
嵌入式 ADC使用手册完整版 ... 嵌入式 ADC使用手册完整版 (188977万字)💜&#...
重大消息战皇大厅开挂是真的吗... 您好:战皇大厅这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游戏...
盘点十款牵手跑胡子为什么一直... 您好:牵手跑胡子这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游...
senator香烟多少一盒(s... 今天给各位分享senator香烟多少一盒的知识,其中也会对sevebstars香烟进行解释,如果能碰...
终于懂了新荣耀斗牛真的有挂吗... 您好:新荣耀斗牛这款游戏可以开挂,确实是有挂的,需要了解加客服微信8435338】很多玩家在这款游戏...
盘点十款明星麻将到底有没有挂... 您好:明星麻将这款游戏可以开挂,确实是有挂的,需要了解加客服微信【5848499】很多玩家在这款游戏...
总结文章“新道游棋牌有透视挂吗... 您好:新道游棋牌这款游戏可以开挂,确实是有挂的,需要了解加客服微信【7682267】很多玩家在这款游...
终于懂了手机麻将到底有没有挂... 您好:手机麻将这款游戏可以开挂,确实是有挂的,需要了解加客服微信【8435338】很多玩家在这款游戏...