个人java学习路线-多线程
什么是多线程
简单的说单线程就是一条路走到死,多线程就是多条路一起走,多线程是实现并发机制的一种有效手段。 值得一提的是在多核计算机出现以前,多线程并发是一种伪并发,是多个线程频繁抢夺cpu时间片造成假象。
再简单说说java中的线程, java程序运行时有几个进程呢?第一时间想到的肯定是main主线程以及GC回收线程。
其他的感兴趣自己百度了解了解。
我们学习需要知道多线程在java种有两个实现方法,一是继承Thread类,二是实现Runnable接口(以及JDK1.5后加入的Callable接口)。
然后掌握下面这张关于线程的生命周期图就行(来自百度,线程学习图都一样)
一.先来看看线程实现
1.继承Thread
结合代码解释:
public class ExtendsTest {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();//线程开始运行,固定格式
for (int i = 0; i <1000 ; i++) {//main方法中的进程
System.out.println("主线程----->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {//run()方法必须复写,线程主体必须写在run()方法中
for (int i = 0; i <1000 ; i++) {
System.out.println("分支线程----->"+i);
}
}
}
看看运行结果:
可见,主线程和分支线程一直在重复抢夺CPU时间片,哪个抢到是随机的。
2.实现Runnable接口
public class RunnableTest {
public static void main(String[] args) {
Thread thread=new Thread(new MyRunnable());
thread.start();
for (int i = 0; i <1000 ; i++) {
System.out.println("主线程----->"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
System.out.println("分支线程----->"+i);
}
}
}
运行结果都差不多。
3.为什么有两种方法
这个当然是因为两个方法有不同的作用。
如果仔细看上面代码就会发现,继承Thread类的方法,每次创建新进程都有一个MyThread类被创建,而实现Runnable接口的方法,每次创建进程,MyRunnable是被共用的。即Runnable接口可以资源共享。(Callable最后再说)
二.线程操作相关方法
1.设置线程名称
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());//获取当前线程名称,这里就是main线程
MyThread myThread=new MyThread();
myThread.setName("一号线程");//myThread线程设置名称为一号线程
System.out.println(myThread.getName());//输出myThread线程名称
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
System.out.println("mt线程---->"+i);
}
}
}
输出结果为:
2.线程睡眠
public class Test2 {
public static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
try {
Thread.sleep(1000);//当前线程睡眠1s=1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果就是1秒打印一次。
3.线程睡眠的唤醒
用interrupt()方法来唤醒
public class Test3 {
public static void main(String[] args) {
SleepThreadTest stt=new SleepThreadTest();
stt.setName("stt");
stt.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stt.interrupt();//唤醒stt线程
}
}
class SleepThreadTest extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+"start");
try {
Thread.sleep(1000*60*60);//睡1小时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"end");
}
}
运行结果:
红色说明,唤醒进程会产生异常。
4.如何停止线程
那么怎么停止进程呢?
自然有专用的方法来让线程死亡,但这并不安全,那么还有什么方法能怎么停止异常?用if加个判断就行
public class Test4 {
public static void main(String[] args) {
StopThread st=new StopThread();
Thread t=new Thread(st);
t.start();
try {
Thread.sleep(500*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
st.run=false;//停止线程
}
}
class StopThread implements Runnable{
boolean run=true;//控制线程的属性
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
if(run){//默认true,运行线程
System.out.println("mt线程---->"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
return;
}
}
}
}
5.线程优先级
优先级越高,抢占CPU时间片能力越强
public class Test5 {
public static void main(String[] args) {
System.out.println(Thread.MAX_PRIORITY);//输出线程优先度最高为10,除此之外还有默认的NORM_PRIORITY优先度为5,和MIN_PRIOROTY优先度为1.
System.out.println(Thread.currentThread().getPriority());//获取当前线程优先级(这里就是main方法优先级),输出为5,即main方法优先级为5
}
}
6.后台线程
线程是独立的,在java程序中,只要前台有一个线程在运行,则整个进程都不会消失,所以此时可以设置一个后台线程,这样即使Java进程结束了,此后台线程依然会继续执行。用setDaemon()方法即可设置后台线程。
public class ProtectThreadTest {
public static void main(String[] args) {
Thread t=new ProtectThread();
t.setName("保护线程");
t.setDaemon(true);//设置后台线程
t.start();
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ProtectThread extends Thread{
@Override
public void run() {
int i=0;
while (true){
System.out.println(Thread.currentThread().getName()+"-->"+(++i));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行后,哪怕t线程是死循环也会结束程序。
7.设置线程"闹钟"
其实就是在固定的时间让线程启动。
public class TimerTest {
public static void main(String[] args) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date firstTime=null;
try {
firstTime=sdf.parse("2021-7-24 15:41:30 000");//改成你想让线程启动的时间,运行程序,等时间到了自动开始。
} catch (ParseException e) {
e.printStackTrace();
}
Timer timer=new Timer();
timer.schedule(new LogTimerTask(),firstTime,1000);
}
}
class LogTimerTask extends TimerTask{
public static int i=0;
@Override
public void run() {
System.out.println(i++);
}
}
三.来说说线程安全
因为多线程是抢夺CPU时间片执行的,这个抢的时候总有各种各样的意外。比如,线程1读取了数据并进行了修改,但在还没修改完时,线程2又读取了数据,这样就会产生安全问题。
1.先来看看一个例子
这是经典的张三取钱的例子:
张三存入10000元,通过线程取钱,一次取5000元。
账户类:
public class Account {
private String actno;
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public void withdraw(double money) throws AccountExcption {
if (this.getBalance()>=money){
this.setBalance(this.getBalance()-money);
System.out.println("账户"+this.getActno()+"取款"+money+"成功,账户还剩"+this.getBalance()+"元");
}else {
throw new AccountExcption("账户余额不足");
}
}
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;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Double.compare(account.balance, balance) == 0 &&
Objects.equals(actno, account.actno);
}
@Override
public int hashCode() {
return Objects.hash(actno, balance);
}
}
异常类:
public class AccountExcption extends Exception{
public AccountExcption() {
}
public AccountExcption(String message) {
super(message);
}
}
线程类:
public class AccountThread extends Thread{
private Account act;
public AccountThread(){
}
public AccountThread(Account act){
this.act=act;
}
@Override
public void run() {
double money=5000;
System.out.println("线程"+Thread.currentThread().getName()+" ");
try {
act.withdraw(money);
} catch (AccountExcption accountExcption) {
accountExcption.getMessage();
}
}
}
main:
public class Test {
public static void main(String[] args) {
Account act=new Account("张三",10000);
AccountThread at1=new AccountThread(act);
AccountThread at2=new AccountThread(act);
at1.start();
/*try {//这里要是去掉注释运行就正常了,但那就不是多线程并发了。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
at2.start();
}
}
先来看看运行结果:
明显有问题,Thread-0运行时,账户应该还剩5000的,然而显示还剩0,这就是线程的安全问题,若不解决这个问题,多拿少拿都有可能。
2.用同步代码块,解决问题:
线程同步也有两种方法
1.同步代码块格式:
synchronized(同步对象){
需同步的代码;
}
2.同步方法格式:
synchronized 方法返回值 方法名称(参数列表){
//方法体
}
****同步对象很宽泛,搞不清用this就行。
****线程同步会锁死一段代码,即,同一时间只允许一个线程访问修改这段代码。
****需注意的是,线程同步会让原本并发的操作变回单线程排队运行,运行效率会下降,所以同步的地方越少越好。
账户类:
public class Account {
private String actno;
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public void withdraw(double money) throws AccountExcption{
synchronized (this){
if (this.getBalance()>=money){
this.setBalance(this.getBalance()-money);
System.out.print("线程"+Thread.currentThread().getName()+" ");
System.out.println("账户"+this.getActno()+"取款"+money+"成功,账户还剩"+this.getBalance()+"元");
}else {
throw new AccountExcption("账户余额不足");
}
}
}
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;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Double.compare(account.balance, balance) == 0 &&
Objects.equals(actno, account.actno);
}
@Override
public int hashCode() {
return Objects.hash(actno, balance);
}
}
其他三个代码都差不多,不用该都行。
看看运行结果:
这样就能解决多线程产生的安全问题
3.用同步方法,解决问题:
public class Account {
private String actno;
private double balance;
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public synchronized void withdraw(double money) throws AccountExcption {
if (this.getBalance()>=money){
this.setBalance(this.getBalance()-money);
System.out.print("线程"+Thread.currentThread().getName()+" ");
System.out.println("账户"+this.getActno()+"取款"+money+"成功,账户还剩"+this.getBalance()+"元");
}else {
throw new AccountExcption("账户余额不足");
}
}
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;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Double.compare(account.balance, balance) == 0 &&
Objects.equals(actno, account.actno);
}
@Override
public int hashCode() {
return Objects.hash(actno, balance);
}
}
差不多,没啥好说的。
四.谈谈死锁
给一个经典例子:
小红说:你先给我苹果,我再给你橘子
小明说:你先给我橘子,我再给你苹果
然后,谁也不给谁僵持住了。
代码:
public class Test {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
Thread t1=new MyThread1(o1,o2);
Thread t2=new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
这个运行就会卡住,就是上面的例子,两个线程都在等对方运行。
所以设计程序时需要多用心,确保不会产生死锁。
五.案例之生产者和消费者
这个例子是为了用多线程答到生产者生成一个物品,消费者消费一个物品,不产生其他问题,主要运用的就是同步。
简单介绍程序:
生产者生产一个石头,线程睡眠,并唤醒消费者线程,
消费者消费一个石头,线程睡眠,并唤醒生产者线程。
代码:
public class Test {
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();
}
}
class Producer implements Runnable{
private List list;
public Producer() {}
public Producer(List list) {
this.list=list;
}
@Override
public void run() {
while(true){
synchronized (list){
if(list.size()>0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add("石头");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"石头+1");
list.notify();
}
}
}
}
class Consumer implements Runnable{
private List list;
public Consumer() {}
public Consumer(List list) {
this.list=list;
}
@Override
public void run() {
while (true){
synchronized (list){
if (list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove("石头");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"石头-1");
list.notify();
}
}
}
}
运行结果: