本文针对多线程,通过为什么使用多线程、多线程优缺点、多线程的实现方式、同步机制来说明多线程的使用;
1 为什么的使用多线程?多线程的作用
就是为了提高程序的运行速度,将主线程主要用于页面交互,子线程用于数据的准备。作者将会在新的博客中使用javaFX案例进行说明;
1、优点:
(1)、使用线程可以把程序中占据时间长的任务放到后台去处理,如图片、视频的下载
(2)、发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好
2、缺点:
(1)、大量的线程降低代码的可读性,
(2)、更多的线程需要更多的内存空间
(3)、当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。
2 多线程的生命周期
按照运行周期,分为新建状态、就绪状态、运行状态、死亡状态,在运行中,可由运行状态转变为阻塞状态,如下图所示
3 多线程的内存运行方式
下面准备了一个简单案例:当main方法执行时,主线程开始进行压栈操作,创建线程t对象,当t执行方法startup方法时,开辟分支栈内存空间,此后,分支栈和主栈为两个栈,互相分离,各自运行,这种运行方式为并发运行;
public class MyThreadTest {
public static void main(String[] args) {
System.out.println("主线程启动运行");
Thread t = new Thread(new myThread01());
t.start();
}
}
class myThread01 implements Runnable{
@Override
public void run() {
System.out.println("分支线程启动运行");
}
}
多线程JVM内存图
4 实现多线程
之前展示了多线程的内存运行,下面介绍多线程的实现方式。实现多线程的方式常用的主要分为两种方式,一种为继承thread,一种为实现Runnable。下面有两个案例。
读者可以使用以后代买进行自测,注意一个小点,如果案例中main方法中的start()方法改为run方法,程序会变成单线程操作,原因是没有使用start()方法开辟分支线程的内存空间;
1、案例一,继承Thread类
public class MyThreadTest02 {
public static void main(String[] args) {
System.out.println("主线程启动运行");
MyThread02 myThread02 = new MyThread02();
myThread02.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程---》"+i);
}
}
}
class MyThread02 extends Thread{
@Override
public void run() {
System.out.println("分支线程启动运行");
for (int i = 0; i < 100; i++) {
System.out.println("分支线程---》"+i);
}
}
}
2、案例二,实现Runnable接口,重写run方法;
public class MyThreadTest03 {
public static void main(String[] args) {
System.out.println("主线程启动运行");
//新建线程对象,使用构造方法传入Runnable方法
Thread thread = new Thread(new MyRunnable03());
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程---》"+i);
}
}
}
class MyRunnable03 implements Runnable{
@Override
public void run() {
System.out.println("分支线程启动运行");
for (int i = 0; i < 100; i++) {
System.out.println("分支线程---》"+i);
}
}
}
5 多线程Api,获取线程名字、获取当前线程对象、sleep方法、终止线程睡眠、终止线程执行、
1、获取当前线程名字,getName()
对于线程名字,当用户不进行设置时,系统会默认设置,格式为Thread-x,主线程默认为main
MyThread02 myThread01 = new MyThread02();
MyThread02 myThread02 = new MyThread02();
MyThread02 myThread03 = new MyThread02();
//输出Thread-0
System.out.println(myThread01.getName());
//输出Thread-1
System.out.println(myThread02.getName());
//输出Thread-2
System.out.println(myThread03.getName());
设置线程名字setName();
myThread01.setName("t1");
myThread02.setName("t2");
myThread03.setName("t3");
//输出t1
System.out.println(myThread01.getName());
//输出t2
System.out.println(myThread02.getName());
//输出t3
System.out.println(myThread03.getName());
2、获取线程对象currentThread();
Thread thread = Thread.currentThread();
3、sleep方法;
在线程代码中调用静态方法sleep(),就可以是该线程进入休眠;
public class MyThreadTest03 {
public static void main(String[] args) {
System.out.println("主线程启动运行");
//新建线程对象,使用构造方法传入Runnable方法
Thread thread = new Thread(new MyRunnable03());
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程---》"+i);
}
}
}
class MyRunnable03 implements Runnable{
@Override
public void run() {
System.out.println("分支线程启动运行");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("分支线程---》"+i);
}
}
}
4、唤醒线程,使用类实例变量调用interrupt(),方法
public class MyThreadTest03 {
public static void main(String[] args) {
System.out.println("主线程启动运行");
//新建线程对象,使用构造方法传入Runnable方法
Thread thread = new Thread(new MyRunnable03());
thread.start();
for (int i = 0; i < 1000; i++) {
if (i==1){
thread.interrupt();
}
System.out.println("主线程---》"+i);
}
}
}
class MyRunnable03 implements Runnable{
@Override
public void run() {
System.out.println("分支线程启动运行");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程---》"+i);
}
}
}
5、合理的终止线程
可以调用静态方法stop,该方法会使得线程中的数据未保存就退出,导致数据丢失。如何合理的终止线程呢?在线程对象中添加实例变量run,在运行中对其进行赋值;
public class MyThreadTest04 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myRunnable.run=false;
}
}
class MyRunnable implements Runnable{
Boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (run) {
System.out.println("分支线程执行--->"+i);
try {
Thread.sleep(1000*1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
/**
* 此处用于保存程序运行中的数据
*/
return;
}
}
}
}
6 线程安全
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*****)
线程安全,什么是线程安全的线程安全问题线程池如何实现;银行账户取款,不安全线程取款(添加yeild,模仿网络延迟),线程安全取款。
如何实现线程安全,使用synchronicized进行修饰。synchronized如何使用。第一种:同步代码块灵活
ynchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
线程安全。
接下来将使用银行卡转账的例子来说明案例:
案例一:线程不安全的转账;
代码部分:
银行账户对象:
public class Account {
//银行卡账户;
private String actNo;
//银行余额;
private Integer balance;
public void withdraw(Integer money){
this.balance = balance-money;
}
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
public Account() {
}
public Account(String actNo, Integer balance) {
this.actNo = actNo;
this.balance = balance;
}
}
账户线程对象:
public class AccountRunnable implements Runnable {
Account account;
@Override
public void run() {
try {
//模仿网络延迟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer money = 5000;
account.withdraw(money);
System.out.println("取款"+money+"成功,"+"余额为:"+account.getBalance());
}
public AccountRunnable(Account account) {
this.account = account;
}
public AccountRunnable() {
}
}
测试程序:
public class AccountThreadTest {
public static void main(String[] args) {
//新建账户对象
Account account = new Account("act-001", 10000);
AccountRunnable accountRunnable = new AccountRunnable(account);
//分三个线程进行模拟
Thread t1 = new Thread(accountRunnable);
t1.start();
Thread t2 = new Thread(accountRunnable);
t2.start();
Thread t3 = new Thread(accountRunnable);
t3.start();
}
}
以上程序运行多次以后出现
取款5000成功,余额为:5000
取款5000成功,余额为:5000
取款5000成功,余额为:5000
即出现错误;
使用synchronized关键字对账户方法中的withdraw方法中的代码块进行修饰:代码如下:
synchronized (this){
this.balance = balance-money;
}
使用synchronized关键字对账户方法中的withdraw方法进行修饰:
public synchronized void withdraw(Integer money){
this.balance = balance-money;
}
总结:
本文主要从为什么要使用多线程、多线程在JVM的内存方式、如何使用多线程,以及多线程带来的线程安全问题如何解决进行了描述。另外,在实际开发中,可以使用以下的策略来解决线程同步的问题:
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。