多线程
一、简单概述
在日常生活中,我们使用最多的是进程。打开任务管理器
可以看到进程的名称、进程ID以及进程的状态!
在操作系统中,将线程划分成程序执行的最小单元,一个进程至少包含一个线程。
看下百度百科的说明:
多线程就是把操作系统中的这种并发执行机制原理运用在一个程序中,把一个程序划分为若干个子任务,多个子任务并发执行,每一个任务就是一个线程。这就是多线程程序。
但是并非是线程越多就越好,因为线程多了,CPU在线程之间来回进行切换,非常浪费时间!
二、创建线程
在java中,万事万物皆时对象,那么java也提供了对应的线程类来提供操作。
继承方式和实现方式
三、开启一条新的线程
如果是继承方式,new XXXX().start()方法来开启一条新的线程;
如果是实现方式,new Thread(实现类对象).start();
Thread类和Runnale接口中有用的方法
Thread.currentThread() // 获取得到当前线程
new Runnable(线程,线程名字);
Thread.sleep(毫秒); 让当前线程暂停,并切换到其它线程;线程休眠方法,这个也是最常使用到的方法
四、线程初入门
例子如下:
public class PrintLn {
public void print1(){
System.out.print("奥");
System.out.print("巴");
System.out.print("马");
System.out.print("是");
System.out.print("总");
System.out.print("统");
System.out.println();
}
public void print2(){
System.out.print("拜");
System.out.print("登");
System.out.print("是");
System.out.print("总");
System.out.print("统");
System.out.println();
}
}
在测试类中进行测试:
public class Test {
public static void main(String[] args) {
new Thread(()->{
// 在这里写上对应的操作方法来进行操作
while (true){
PrintLn p = new PrintLn();
// 不断调用print1方法
p.print1();
}}).start();
new Thread(()->{
// 在这里写上对应的操作方法来进行操作
while (true){
// 不断调用print1方法
// PrintLn p1 = new PrintLn();
PrintLn p = new PrintLn();
p.print2();
}}).start();
}
}
截取控制台输出的一段结果:
奥巴马是总统
是总统
拜登是总统
奥巴马是总统
为什么会看到是总统三个字,我们期望看到的是奥巴马是总统或者是拜登是总统,这里说明了线程在进行执行的时候,线程发生了切换。
在这里我举个例子来进行说明,假设是线程一发生了切换
线程一在执行输出的时候,正准备输出奥巴马是总统中的是的时候,发生了线程切换,结果就是上面控制台输出的结果了。
但是我想看到的结果是一个方法中完整的输出,那么如果来实现???
使用同步代码块来进行解决!!
public class PrintLn {
public void print1(){
synchronized ("aaa"){
System.out.print("奥");
System.out.print("巴");
System.out.print("马");
System.out.print("是");
System.out.print("总");
System.out.print("统");
System.out.println();
}
}
public void print2(){
synchronized ("aaa") {
System.out.print("拜");
System.out.print("登");
System.out.print("是");
System.out.print("总");
System.out.print("统");
System.out.println();
}
}
}
加上了synchronized关键字可以来解决这个问题。
synchronized(锁对象){
同步的代码块;
}
一定要注意小括号中的锁对象!!!对锁对象的要求就是同一把锁
什么是同一把锁??在程序运行过程当中,这把锁一直保持不变,一直都是同一个。上面的同步代码块中使用的就是同一个锁"aaa",在程序运行过程中,字符串"aaa"在内存中就只有一个,从而在多线程情景下,让每个方法执行中将方法执行完成之后,才会去执行下一个方法;
最常使用的锁对象:
this;当前类的字节码对象;Lock锁;同步方法(默认是当前类的字节码对象)
使用前提是:一定要保持锁对象唯一,这里不在来进行演示,按照上面的例子可以来进行模仿操作。
五、线程安全问题
什么是线程安全?为什么会有线程安全?
先来总结一下造成线程安全的原因:
1、多线程环境下;
2、共享变量;
3、多线程操作共享变量;
举个比较简单的例子来进行说明:
public class SafeThread extends Thread {
private static int num = 0;
@Override
public void run() {
for (int i = 0; i < 300; i++) {
num++;
System.out.println(num);
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 开启三百个线程。共有300*300=90000
for (int i = 0; i < 300; i++) {
new SafeThread().start();
}
}
}
控制台结果截取最终结果:
89999
多执行几次
89995
89942
发现最终执行的结果不同
但是我想要看到的是多线程情况下,最终得到的结果是90000
那么如果利用上面的同步代码块来解决问题,修改代码如下
public class SafeThread extends Thread {
private static int num = 0;
@Override
public void run() {
for (int i = 0; i < 300; i++) {
synchronized (SafeThread.class){
num++;
}
System.out.println(num);
}
}
}
多次显示结果还是90000。符合多线程安全问题的解决方式
1、多线程条件下;
2、共享数据是num;
3、多线程操作num变量;(操作语句是num,输出语句并不影响)
5.1、买票案例
public class SellTicket implements Runnable{
// 票数
int ticketNum = 100;
@Override
public void run() {
// 对买票来进行操作
// 逆向思维考虑一下;如果没有票了就不卖了
while (true){
synchronized (SellTicket.class){
// 如果没有票了,直接关掉
if (ticketNum<=0){
break;
}
ticketNum--;
}
System.out.println(Thread.currentThread().getName()+"---->"+ticketNum);
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
new SellTicketDemo1().start();
new SellTicketDemo1().start();
new SellTicketDemo1().start();
}
}
使用这种方式来解决线程安全问题。
5.3、转账案例
共享变量:Account类
public class Account {
public static Integer money = 10000;
}
线程一:
public class Jerry extends Thread {
@Override
public void run() {
while (Account.money==10000){
}
System.out.println("没有十万了!!!!!");
}
}
线程二:
public class Tom extends Thread {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 取走一百块钱
Account.money-=10000;
System.out.println("我把钱取走了");
}
}
测试类:
public class AccountTest {
public static void main(String[] args) {
Jerry jerry = new Jerry();
Tom tom = new Tom();
new Thread(jerry).start();
new Thread(tom).start();
}
}
查看控制台:
我把钱取走了
Tom把钱取走了,但是Jerry一直没有发现钱被取走了。
画图解释原因
最终导致了Account中的数据发生了改变,但是Jerry没有发现money改变了。这个是什么原因导致的呢?
这里涉及到了JVM内存模型
主内存和工作内存
上面的Account中的money就属于是主内存中的数据;Tom和Jerry中的money就是工作内存中的数据。工作内存中的money就是主内存中的money的一份拷贝。(操作系统中的内容)
Tom改变了自己工作内存中的数据之后,然后又将值同步到了内存中去;但是对于Jerry来说,它不知道主内存中的数据发生了变化,还以为主内存中的数据是100000。
我们想要得到结果是:如果主内存中的数据发生了改变,那么各个线程都需要立马得知到主内存中的数据发生改变了!!!马上去同步最新的数据到自己的工作内存中来。
非常简单,只需要一个简单的关键字即可解决
volatile
将Account中的代码进行修改
public class Account {
public volatile static Integer money = 10000;
}
再次执行测试类的内容
我把钱取走了
没有十万了!!!!!
那么这里就解决了!!!!
5.3、volatile
和synchronized做比较,synchronized相对来说是一个重量级别的,而volatile则是一个轻量级别的。
volatile:真正的含义,工作内存中的数据将主内存中的数据拷贝到自己的工作内存之后,修改完成之后,想要同步到主内存中去,同步了数据之后,但是其他的线程不知道主内存中的数据发生了改变,他们一直以为当前的数据就是主内存中的数据。
那么加上了volatile关键字之后,这个关键字起到的作用就是通知其他线程,主内存中的共享数据发生改变了,你们自己去获取;然后其他线程收到了通知,就去主内存中获取共享数据最新的值,然后进行操作。
总结
这块是比较基础的案例,也是最简单的操作。
最好的实践方式就是看看源码中的代码如何写的。
参考StringBuff和ConcurrentHashMap