多线程
1.1、什么是进程?什么是线程?
进程是一个应用程序(1个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
1.2、对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。
1.3、进程和线程是什么关系?举个例子
阿里巴巴:进程
马云:阿里巴巴的一个线程
童文红:阿里巴巴的一个线程
京东:进程
强东:京东的一个线程
妹妹:京东的一个线程
进程可以看做是现实生活当中的公司。
线程可以看做是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)
魔兽游戏是一个进程
酷狗音乐是一个进程
这两个进程是独立的,不共享资源。
线程A和线程B呢?
在java语言中:
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,
互不干扰,各自执行各自的,这就是多线程并发。
火车站,可以看做是一个进程。
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
所以多线程并发可以提高效率。
java中之所以有多线程机制,目的就是为了提高程序的处理效率。
1.4、思考一个问题:
使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在
压栈弹栈。
1.5、分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。
1.6 第一种创建线程的方法
编写一个类,直接继承java.lang.Thread,重写run方法。
- 创建线程对象?new就行
- 怎么启动?调用线程对象run方法
public class ThreadTest02 {
public static void main(String[] args) {
这里属于主线程,在主栈种运行。
新建一个分支线程对象
MyThread myThread = new MyThread();
启动线程
start方法:启动一个分支线程,在JVM开辟一个新的栈空间,完成任务瞬间结束
栈空间开出来,start就结束,线程启动成功。
启动成功的线程自动调用run方法,压栈(栈底部)
run在分支栈底部,main方法在主栈的栈底部。两者平级
myThread.start();
这里的代码还是在主线程种
for(int i=0; i<1000;i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run(){
编写程序,这段运行在分支线程种(分支栈)
for(int i=0; i<1000;i++){
System.out.println("分支线程--->" + i);
}
}
}
1.7 第二种
实现线程的第二种方式,编写一个类实现java.lang.Runnable接口
public class ThreadTest03 {
public static void main(String[] args) {
//创建一个可运行的对象
MyRunnable r = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
//启动线程
t.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);
}
}
}
也可以用匿名内部类的方式
//匿名内部类实现
public class ThreadTest04 {
创建线程对象,匿名内部类方式
通过一个没有名字的类,new出来的对象 这里是一个类,不是一个接口
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<100; i++){
System.out.println("t线程");
}
}
});
//启动线程
t.start();
for (int i=0; i<100; i++){
System.out.println("main线程");
}
}
}
1.7 获取和修改线程名字 以及当前线程对象
setName getName Thread.currentThread()
public class ThreadTest05 {
public static void main(String[] args) {
//这个代码出现在main方法种,所有当前线程就是主线程。
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName());
MyThread2 t = new MyThread2();
t.setName("t1");
String tName = t.getName();//默认是Thread-0
System.out.println(tName);
t.start();
MyThread2 t2 = new MyThread2();
t2.setName("t2");
System.out.println(t2.getName());//默认是Thread-1
t2.start();
}
}
class MyThread2 extends Thread{
public void run(){
for(int i=0; i<1000;i++){
//当前线程对象
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() + "分支线程--->" + i);
}
}
}
线程的sleep方法
线程的sleep方法
- static void sleep(long millis)
- 1、静态方法
- 2、参数是毫秒
- 3、让当前线程进入休眠,进入“阻塞状态”,放弃所有CPU时间片,让给其他线程。
public class ThreadTest06 {
public static void main(String[] args) {
//创建线程
Thread t = new MyThread3();
t.setName("t");
t.start();
//调用sleep方法
try {
t.sleep(5*1000);只会让main线程进入休眠,不会让t睡眠
执行的时候还是会转化成:Thread.sleep
} catch (InterruptedException e) {
e.printStackTrace();
}
5秒之后这里才会执行
System.out.println("hello world!");
}
}
class MyThread3 extends Thread{
public void run(){
for(int i=0; i<1000;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
叫醒
这里注意 子类重写父类方法,run中的sleep方法只能trycatch 不能抛出异常
因为run在父类中没有抛出异常,所以子类不能比父类抛出更多异常。
这种中断睡眠的方式依靠了java的异常处理机制
t.interrupt();
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
//希望5秒之后,t线程醒来
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这种中断睡眠的方式依靠了java的异常处理机制
t.interrupt();
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->begin");
try {
//这里注意 子类重写父类方法,只能trycatch 不能抛出异常
Thread.sleep(1000 * 60 * 60 * 24);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->end");
}
}
终止一个线程
stop可以强行终止线程,会丢失数据,不建议使用。
设一个boolean值,判断一下,如果为false,return就好了
public class ThreadTest09 {
public static void main(String[] args) {
MyRunnable3 r = new MyRunnable3();
Thread t = new Thread(r);
t.setName("t");
t.start();
//模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
r.run=false;
}
}
class MyRunnable3 implements Runnable{
boolean run = true;
@Override
public void run() {
for(int i=0; i<10; i++){
if (run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//终止当前线程
return;
}
}
}
}
2、线程的安全问题
2.1、为什么这个是重点?
以后在开发中,我们的项目都是运行在服务器当中,
而服务器已经将线程的定义,线程对象的创建,线程
的启动等,都已经实现完了。这些代码我们都不需要
编写。
最重要的是:你要知道,你编写的程序需要放到一个
多线程的环境下运行,你更需要关注的是这些数据
在多线程并发的环境下是否是安全的。(重点:*****)
2.2、什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
*条件1:多线程并发。
*条件2:有共享数据。
*条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
2.4、说到线程同步这块,涉及到这两个专业术语:
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
异步就是并发。
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
同步就是排队。
实例
关于锁机制
synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据。才能达到多线程排队。
()中写什么?
那要看你想让哪些线程同步。
假设t1、t2、t3、t4、t5,有5个线程,
你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
你一定要在()中写一个t1 t2 t3共享的对象。而这个
对象对于t4 t5来说不是共享的。
这里的共享对象是:账户对象。
账户对象是共享的,那么this就是账户对象吧!!!
不一定是this,这里只要是多线程共享的那个对象就行。
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
100个对象,100把锁。1个对象1把锁。
以下代码的执行原理?
1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
t2占有这把锁之后,进入同步代码块执行程序。
这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
执行的这些线程对象所共享的。
账户类
这里synchronized放this obj 字符串都可以,因为都是t1、t2共享的
package com.bjpowernode.java.threadsafe;
//银行账户
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;
}
//对象
Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)
//取款方法
public void withdraw(double money){
//t1和t2并发这个方法。。。(两个栈,操作堆中同一个对象)
//线程同步 传入的数据必须是多线程共享的数据,才能达到多线程排队
//synchronized (this){
//synchronized (obj){
synchronized ("abc"){//字符串常量池中,只有一个
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//t1执行到这里,还没来得及执行这行代码更新,t2线程进来withdraw方法了,此时一定出问题
this.setBalance(after);
}
}
}
线程类
public class AccountThread extends Thread {
//两个线程必须共享同一个账户对象。
private Account act;
public AccountThread(Account act) {
this.act = act;
}
public void run(){
//run方法的执行表示取款操作。
//取款5000
double money = 5000;
act.withdraw(5000);
System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款成功,余额" + act.getBalance());
}
}
测试
public class Test {
public static void main(String[] args) {
//创建账户对象
Account act = new Account("act-001",10000);
//两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
注意synchronized 锁也可以写在run方法里,都是一样的。
但是这样扩大了同步的范围,效率更低了
这里不能写this,代表的是线程类AccountThread
public void run(){
//run方法的执行表示取款操作。
//取款5000
double money = 5000;
synchronized (act){
act.withdraw(5000);
}
System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款成功,余额" + act.getBalance());
}
还可以写在实例方法上
将账户Account类withdraw方法改为
出现在实例方法上,一定锁的是this 不灵活,不建议,但有用
//出现在实例方法上,一定锁的是this 不灵活
public synchronized void withdraw(double money){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
3、Java中有三大变量?【重要的内容。】
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
4、如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。
5、总结:
synchronized有三种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
小练习
doOther方法执行时需要等待doSome方法的结束吗?
不需要,因为doOther()方法没有synchronized
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass{
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
需要 synchronized出现在静态方法上是类锁,不管创建几个对象,锁只有一把
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass{
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther(){
System.out.println("doOther begin");
System.out.println("doOther end");
}
}
死锁
开发中synchronized最好不要嵌套使用
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
//t1 t2两个线程共享o1 o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
}
}
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 {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
这块代码结束,锁才取消,然而锁o2的时候,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(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
锁o1的时候,o1被占用了
}
}
}
}
6、聊一聊,我们以后开发中应该怎么解决线程安全问题?
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
就只能选择synchronized了。线程同步机制。
守护线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,
守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,没到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。
public class test {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
设置为守护线程,主线程结束,守护就结束了
t.setDaemon(true);
t.start();
//主线程:这是用户线程
for (int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
public void run(){
int i = 0;
while (true){
System.out.println(Thread.currentThread().getName() + "--->" +(++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。
每天要进行数据的备份操作。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
定时任务的。
在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,
这个框架只要进行简单的配置,就可以完成定时器的任务。
public class TimeTest {
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-06-03 20:20:00");
timer.schedule(new LogTimerTask(),firstTime,1000*10);
}
}
class LogTimerTask extends TimerTask{
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}