前言:
今天给大家分享一些自己在学习JavaSE阶段中,学到多线程这个阶段之后,
自己所总结的一些知识点,希望对您有所帮助,并且自己也是个初学者,
有什么地方写错了,也希望您能点出来。另外,自己最近也在开始写
博客,希望在学习的道路上一起进步,也期待您的关注与支持。
文章目录
1、进程和线程
2、什么是真正的多线程并发
3、实现线程的3种方式
4、线程对象的生命周期
5、线程的调度
6、数据的安全性问题
7、异步编程模型和同步编程模型
8、Java语言三大变量与线程安全
9、synchronized关键字
10、守护线程
11、定时器
12、关于Object类中的wait和notify方法(生产者和消费者模式)
13、死锁
14、自己以后遇到线程安全怎么办
一、进程和线程
1.1、什么是进程?什么是线程?
进程是一个应用程序(1个进程是一个软件)
线程是一个程序的执行场景/执行单元
一个进程可以启动多个线程。
1.2、进程和线程的关系?举例说明。
阿里巴巴:可以看成一个进程
马 云:可以看成一个线程
京 东:可以看成一个进程
刘 强 东:可以看成一个线程
所以:进程可以看做是现实生活中的公司。线程可以看做公司中的某个员工。
注意:进程A和进程B的内存独立不共享。
线程A和线程B的方法区内存和堆内存共享,栈内存相互独立。
Java中之所以有多线程机制,目的就是为了提高程序的处理效率。
二、什么是真正的多线程并发
t1线程执行t1的。
t2线程执行t2的。
t1不会形象t2,t2也不会影响t2的,这才叫真正的多线程并发。
三、实现线程的3种方式
3.1、第一种方式:
编写一个类直接继承Java.lang.Thread,重写run()方法。
// 定义线程类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程
t.start();
3.2、第二种方式:
编写一个类,实现java.lang.Runnable接口,实现run方法。
// 定义一个可运行的类
class MyRun implements Runnable{
@Override
public void run() {
}
}
// 创建线程对象
Thread t = new Thread(new MyRun());
// 启动线程
t.start();
3.3、第三种方式:
FutureTask方式,实现Callable接口。(JDK8新特性)
这种方式实现的线程可以获取线程的返回值。
之前的那两种方式是无法获取线程的返回值的,因为run方法返回void。
// 第一步:创建一个“未来任务类”对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call方法就相当于run方
return null;
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 获取线程的返回值
Object obj = null;
try {
obj = task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程执行结果"+obj);
// main方法这里的程序需要执行到这里必须等待get()方法的结束
// 而get()方法可能需要很久,因为get()方法是为了拿到另一个线程的执行结果
// 另一个线程执行是需要时间的。
// 所以get()可能会导致当前线程进入阻塞状态。
System.out.println("hello");
四、线程对象的生命周期
抢占式调度模型:
哪个线程的优先级比较高,抢到的CPU时间片的概率就比较高/多一写。
Java采用的就是这种模型
均分式调度模型:
平均分配CPU时间片,每个线程占用的CPU时间片长度一样。
平均分配,一切平等。
有一些编程语言,线程调度模型就是采用的这种方式。
5.2、Java提供的哪些方法是和线程调度有关系呢?
实例方法:
void setPriority(int newPriority) // 更改线程的优先级。
int getPriority() // 返回线程的优先级。
// 最低优先级 1
// 最高优先级 10
// 默认优先级 5
void join() // 合并线程。
class MyThread extends Thread{
public void doSome(){
MyThread2 t = new MyThread();
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程执行完之后,当前线程才可以执行。
}
}
class MyThread2 extends Thread{
}
静态方法:
static void yield() // 让位方法
/*暂停当前正在执行的线程对象,并执行其他线程。
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪状态之后,还有可能再次抢到CPU时间片进入运行状态。*/
六、线程的安全性问题
6.1、线程安全的重要性?
以后在开发中,我们的项目都是运行在服务器中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了,这些代码我们不需要编写。
最重要的是:你要知道,你编写的程序需要放在一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点*****)
6.2、什么时候数据在多线程并发的环境下存在安全性问题呢?
三个条件:
条件一:多线程并发
条件二:有数据共享
条件三:共享数据有修改的行为
满足以上三个条件之后,就会存在线程安全问题。
6.3、怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,
怎么解决这个问题呢?
线程排队执行。(不能并发)
用排队解决线程安全问题。
这种机制被称为:线程同步机制。
专业术语叫:线程同步,实际上就是线程不能并发,线程必须排队执行。
怎么解决线程安全问题呢?
使用“线程同步机制”。
线程同步就是线程排队了,线程排队就会牺牲一部分效率,没办法,数据安全第一位,
只有数据安全了,我们才可以谈效率,数据不安全没有效率的事。
七、异步编程模型和同步编程模型
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不用管t2,t2不用管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实也就是:多线程并发(效率较高)
异步就是并发
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待线程t2执行结束,
或者说在线程t2执行的时候,必须等待线程t1执行结束,两个线程之间
发生了等待关系,这就是:同步编程模型。
效率较低,线程同步执行。
同步就是排队
八、Java语言三大变量与线程安全
实例变量:在堆中
静态变量:在方法区中
局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量永远都不会共享。(一个线程一个栈)
局部变量在栈中,所以局部变量永远不会共享。
实例变量和静态变量都可能存在线程安全问题。
局部变量 + 常量 不会有线程安全问题
成员变量可能会有线程安全问题
九、synchronized关键字
9.1、synchronized有三种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this,并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有一把。
就算创建了100个对象,哪类锁也只有1把。
对象锁:1个对象1把锁,100个对象100把锁
类锁:100个对象,也可能只有1把锁
9.2、举例说明:
例子1:
/**
* doOther方法的执行需不需要等待doSome方法的结束吗?
*
* 不需要,因为doOther方法上没有synchronized,不需要获取对象锁。
*/
public class Exam01 {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);// 这个睡眠的作用是保证t1线程先执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
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{
// synchronized 出现在实例方法上,锁的是this
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
例子2:
/**
* doOther方法的执行需不需要等待doSome方法的结束吗?
*
* 需要,synchronized在实例方法上锁的是this
*/
public class Exam01 {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);// 这个睡眠的作用是保证t1线程先执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
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{
// synchronized 出现在实例方法上,锁的是this
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
例子3:
/**
* doOther方法的执行需不需要等待doSome方法的结束吗?
*
* 不需要,MyClass对象是两个,两把锁。
*/
public class Exam01 {
public static void main(String[] args) {
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();
try {
Thread.sleep(1000);// 这个睡眠的作用是保证t1线程先执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
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{
// synchronized 出现在实例方法上,锁的是this
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
例子四:
/**
* doOther方法的执行需不需要等待doSome方法的结束吗?
*
* 需要,静态方法是类锁,类锁不管创建了几个对象,类锁只有一把。
*/
public class Exam01 {
public static void main(String[] args) {
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();
try {
Thread.sleep(1000);// 这个睡眠的作用是保证t1线程先执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
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{
// synchronized 出现在静态方法上,锁的是类
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
十、守护线程
10.1、Java语言中线程分为两大类:
一类是: 用户线程
另一类是:守护线程(后台线程)
其中具有代表性的是:垃圾回收线程(守护线程)
10.2、守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
10.3、注意:主线程main是一个用户线程。
10.4、语法:
// 线程对象.setDaemon(true);
10.4、举例子:
/**
* 守护线程
* t.setDaemon(true);
*/
public class ThreadTest14 {
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();
}
}
}
}
运行结果:
main---->0
备份线程---->1
main---->1
备份线程---->2
main---->2
备份线程---->3
备份线程---->4
main---->3
main---->4
备份线程---->5
main---->5
备份线程---->6
main---->6
备份线程---->7
main---->7
备份线程---->8
备份线程---->9
main---->8
main---->9
备份线程---->10
备份线程---->11
Process finished with exit code 0
十一、定时器
11.1、定时器的作用是什么:
间隔特定的时间去执行特定的程序。
比如:每天进行银行账户的总账操作。
每天进行数据的备份操作等。
11.2、实现方式:
可以使用sleep方法,睡眠,设置睡眠时间,每到某个时间点醒来,执行任务
。这种方法是最原始的定时器。(比较low)
在Java的类库已经写好一个定时器:java.util.Timer,可以拿来直接用。
不过,这种方式在目前的开发中也很少用,因为现在有好多高级框架都是支持
定时任务的。
在实际的开发中,目前使用的最多是Spring框架的SpringTask框架,
这个框架只需要简单的配置,就可以完成定时器的任务。
11.3、举例子:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* 使用定时器创建定时任务
*/
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
// Timer timer = new Timer(true); // 守护线程的方式
// 指定定时任务
// timer.schedule(定时任务,第一次执行的时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = null;
try {
firstTime = sdf.parse("2021-1-22 21:29:50");
} catch (ParseException e) {
e.printStackTrace();
}
timer.schedule(new 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 + "-->完成一次数据备份!");
}
}, firstTime, 1000 * 10);
}
}
运行结果:
2021-01-22 21:29:07-->完成一次数据备份!
2021-01-22 21:29:17-->完成一次数据备份!
2021-01-22 21:29:27-->完成一次数据备份!
十二、关于Object类中的wait和notify方法(生产者和消费者模式)
12.1、wait和notify方法不是线程对象的方法,是Java中任何一个java对象都有的方法,因为着两个方式是Object类中自带的。
wait方法和notify方法不是通过线程对象调用。
12.2、wait()的作用?
Object o = new Object();
o.wait();
/*
表示:
让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让当前线程进入等待状态。
*/
12.3、notify()的作用?
Object o = new Object();
o.notify();
/*
表示:
唤醒正在o对象上等待的线程。
*/
/*
还有一个o.notifyAll();方法:
这个方法是唤醒o对象上处于等待的所有线程。
*/
12.4、举例子:
/*
*模拟这样一个需求:
* 仓库采用List集合。
* 1个元素就代表仓库满了。
* 如果List集合中个数为0,表示仓库空了。
* 保证List集合永远最多储存1个元素。
*
* 必须做到这种效果:生产一个消费一个。
*/
public class ThreadTest16 {
public static void main(String[] args) {
List list = new ArrayList();
Thread t1 = new Thread(new Produce(list));
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程生产");
t2.setName("消费者线程消费");
t1.start();
t2.start();
}
}
// 生产线程
class Produce implements Runnable{
// 仓库
private List list;
public Produce(List list){
this.list = list;
}
@Override
public void run() {
while (true){ // 使用死循环来一直生产
synchronized (list){ // 给仓库对象list加锁
if (list.size() > 0){ // 大于0,说明仓库中已经有1个元素了
try {
// 当前线程进入等待状态,并且释放Produce之前占有的list对象锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明可以生产
Object obj = new Object();
list.add(obj);
System.out.println(MyThread3.currentThread().getName()+"--->"+obj);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒消费者线程进行消费
list.notify();
}
}
}
}
// 消费线程
class Consumer implements Runnable{
// 仓库
private List list;
public Consumer(List list){
this.list = list;
}
@Override
public void run() {
while (true){ // 一直消费
synchronized (list){
if (list.size() == 0){ // 仓库空了
try {
// 消费线程进行等待,释放掉list集合占有的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里说明仓库中有数据,可以进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒生产者线程进行生产
list.notify();
}
}
}
}
12.5、运行结果:
生产者线程生产--->java.lang.Object@528ff726
消费者线程消费--->java.lang.Object@528ff726
生产者线程生产--->java.lang.Object@5c481307
消费者线程消费--->java.lang.Object@5c481307
生产者线程生产--->java.lang.Object@58779c56
消费者线程消费--->java.lang.Object@58779c56
生产者线程生产--->java.lang.Object@1d75193c
消费者线程消费--->java.lang.Object@1d75193c
生产者线程生产--->java.lang.Object@30eeeb73
消费者线程消费--->java.lang.Object@30eeeb73
生产者线程生产--->java.lang.Object@2c220642
消费者线程消费--->java.lang.Object@2c220642
生产者线程生产--->java.lang.Object@4853aabc
消费者线程消费--->java.lang.Object@4853aabc
十三、死锁
13.1、代码
public class DeadLock {
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;
}
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} 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;
}
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
死锁:死锁这种情况下,程序不会报错,不会产生任何错误,程序会一直僵持在那里。所以遇到这种情况,程序非常难调试。通过以上的代码,我们可以得出一个结论:synchronized在开发中最好不要嵌套使用,因为一不小心就有可能造成死锁的现象。
14、自己以后遇到线程安全怎么办
是一上来就选择线程同步机制吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量/并发量降低。用户体验差。在不得已的情况下在选择
线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应一个对象,100个
线程对应100个对象,对象不共享就没有数据安全问题)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择
synchronized,线程同步机制。
最后
希望在学习的道路上一起进步,也期待您的关注与支持。