java的内存分配与管理
如果要学习多线程,我们必须先对java的内存分配和管理有一定的了解
java的分区包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池这几个部分。
栈区
栈区有以下几个特点:
1、线程私有,每个线程都会拥有自己的栈
2、每个方法在被调用时,都会有一个独立的栈帧,而当方法执行完毕后,其对应的栈帧会被弹出并丢弃。
3、栈的存取速度会比堆块
栈一般会存放java中的基本类型的变量数据、对象的引用变量、实例方法的调用和执行信息
引用引用!这一点挺关键。最关键的就是在运行时能够找到相应的对象或方法。
堆区
堆区有以下几个特点:
1、堆区是内存共享的,也就是说存在堆区的资源,线程都能拿来使用
2、堆区的内存是动态分配的,即在程序运行时根据需要动态地申请内存空间。
3、是内存空间中占空间最大的一块
4、堆区物理上不连续,逻辑上连续(熟悉的《操作系统》哈哈哈)
堆区一般会存放java的实例对象。在Java中,通过new关键字创建的对象实例都会在堆上分配内存空间。这些对象实例包括类的实例、数组等。
注意,只有实例化后的对象才会出现在堆区,仅仅加载的类是不会在堆区的。
方法区
方法区的特点:
1、方法区是各个线程共享的内存区域,这意味着所有线程都可以访问方法区中的数据
2、具有一个常量池
方法区用于存储已被虚拟机加载的类型信息(如类Class、接口Interface、枚举Enum、注解Annotation等)、常量、静态变量、即时编译器编译后的代码缓存等
常量池
可以理解成一张表,JVM指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息。常量池包括数量值、字符串值、类引用、字段引用、方法引用等。
多线程
线程与进程
线程概念
在Java中,线程是程序执行流的最小单元,是基本的CPU执行单元。线程的存在是为了提高程序的运行效率,是并发执行的基本单位。
两者区别
线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程设计出来就就是为了防止两程序之间彼此干扰,于是设置较高的独立性,资源是互不共享的。
而为了提高线程的并发性,提高效率,线程之间的独立性是较低的,资源可以共享。
JVM资源共享区域
堆和方法区可以让线程之间资源共享,计数器、JVM栈、本地方法栈不行
线程状态
一个线程被创建出来可能会处在以下几个状态中
新建
线程被创建出来之后,未start之前的状态
就绪
线程start(),线程在就绪时,会在队列中等待,等待时间片的分配,一旦有时间片(得到调度)run方法就会执行,进入运行状态
创建线程的两种简单方式
第一种:1、编写一个类继承java.lang.Tread 2、重写run方法 3、new线程对象后start
第二种方式: 1、编写一个类实现java.lang.Runnable接口 2、实现接口中的run方法 3、new线程对象后start
class Cat extends Thread{
@Override
public void run() {//重写run方法,写上自己的业务逻辑
int times = 0;
while (times<10) {
times++;
System.out.println(times+":");
System.out.println("喵喵");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class food implements Runnable{
String foodName = "meat";
public void setFoodName(String foodName) {
this.foodName = foodName;
}
public static int num = 100;
@Override
public void run() {
while (num>0) {
num--;
System.out.println(num);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//创建Cat对象,可以当作线程使用
Cat cat = new Cat();
food fo = new food();
//需要把food交给一个线程
Thread thread1 = new Thread(fo);
thread1.start();
cat.start();
线程start()将意味着线程进入就绪状态,等待JVM调度。
Thread的类是实现了Runnable接口的
另外,我们可以采用匿名内部类和lambda表达式的方式来创建
public class Thread02 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 10 ; i++) {
System.out.println("aaa");
}
}
});
t.start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 9; i++) {
System.out.println("bbb");
}
}
}).start();
//lambda表达式
new Thread(()-> System.out.println("!!!!!!!!!!!!!!!!")).start();
}
}
运行
在线程得到调度时,线程会执行run方法
@Override
public void run() {
System.out.println("wang!");
}
至于什么时候得到调度需要看JVM的脸色。
阻塞
线程暂时挂起。进入该状态是因为遇到了对象锁,需要等待获得对象锁才能解除该状态,重新回到就绪状态。
将线程变成阻塞状态可以使用synchronized关键字来实现。
public synchronized void get()
上面的方法就是加入了synchronize关键字,表明如果有线程需要执行到这个方法,线程需要排队,无法并发执行该方法。
另外,join()线程合并也可以产生让线程被阻塞的效果。join()方法是一个实例方法。假设在main方法中调用了t.join()后果是让t线程加入到主线程中,主线程进入阻塞状态,直到t线程执行结束,或者是合并时间结束。当前线程进入阻塞,直到t结束,当前线程解除阻塞
ublic class JoinTest {
public static void main(String[] args) {
myThread t = new myThread();
t.start();
//合并线程,t合并到当前线程中,当前线程受到阻塞
//join(millis),合并线程millis毫秒,t执行结束,解除阻塞
try {
t.join(8);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"----->"+i);
}
}
}
class myThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
等待
在这个状态下表明线程正在等待input或者等待其他指令执行,这个等待的时间没有限定,可能会等一辈子(狗头)。只有等到了需要的指令,线程才会解除等待回到就绪状态。wait()。
超时等待
这个状态是指线程进入了休眠,睡着了,需要等待唤醒。这个睡眠时间有限定,定了闹钟,到点了就可以回归就绪状态了。
可以使用Thread.sleep()让当前线程进入休眠状态,因为该方法是一个静态方法,它由类名调用的。
class banana extends Thread{
public banana(String name) {
super(name);
}
@Override
public void run() {
try{
Thread.sleep(800000*201);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wake up!!!!!!");
}
}
注意这里需要抛出或处理异常消息喔
然后,我们也可以提前唤醒线程,使用t.interrupt()来唤醒,这个是一个实例方法,让指定线程被唤醒。
实现原理利用了java的异常处理机制,当线程需要唤醒时,立马产生异常,打断线程休眠,然后异常被处理之后会正常执行后续的代码,以达到线程唤醒又不妨碍其他代码块执行的目的。
try {
Thread.sleep(500);
a.run = false;
b.interrupt();//会抛出Interruption异常,以打断线程执行,处理异常后不影响后续代码的执行
System.out.println("Main end");
} catch (Exception e) {
e.printStackTrace();
}
终结
线程执行完毕后,run方法结束了,线程就终结了。线程的栈空间就会自动被回收。
线程类型
用户进程
指非守护进程,是创建出来的一般进程,可以调用其提供的一些方法。如:setName,interrupt等。Main方法也属于用户进程。
守护进程
守护进程就是那个默默干活的老铁,最典型的守护线程就是java的垃圾回收线程,可以将用户进程和守护进程的关系理解为舞台上的表演者和幕后的后勤人员。守护进程会在用户进程全部执行完毕后终结。
若要创建守护进程,使用setDaemon()将参数设置为true即可
Orange o = new Orange();
o.setDaemon(true);
线程调度
Java的线程调度策略
一般调度策略可以分为非抢占形调度策略和抢占形调度策略。
非抢占形调度策略是以时间片(CPU使用时间)来轮转调度的,非抢占式调度策略确保进程或线程在没有阻塞事件的情况下持续执行,不会被其他更高优先级的任务中断。
而抢占形调度策略会允许线程被其他更高优先级的任务中断。
java的线程调度策略是抢占形调度策略,这种策略可以应对一些突发的高优先级的任务,达到快速响应,但是代价就要花费更多的系统开销。
优先级
线程具有优先级,优先级高的线程获得系统调度的可能性就会大,并不是优先级高就一能一直占着时间片。java中的优先有MAX(10) NORMAL(5) MIN(1)这三种。
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.NORM_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
//设置优先级
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
System.out.println(Thread.currentThread().getPriority());
线程优先级是可以设置的,使用setPriority()进行设置即可,但注意不要超出Max到Min的范围,否则是会报异常的
java源码如下:
让位
静态方法 yield()让当线程放弃当前获得的时间片,只能是大概率地让位一次
定时任务
java线程可以设置定时任务,使用Timer和TimerTask组合来实现。定时任务,使用jdk提供的计时器实现 Timer(),可以结合TimerTask()定时任务 。来实现每间隔多久执行一次任务。在实际开发中会使用框架SpringTask这种,但是实现原理都是这样的。
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建计时器对象(本质上就是一个线程)
Timer timer = new Timer(true);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = simpleDateFormat.parse("2024-06-07 11:00:00");
timer.schedule(new LogTimerTask(),date,1000);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class LogTimerTask extends TimerTask{
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
@Override
public void run() {
Date now = new Date();
String strTime = simpleDateFormat.format(now);
System.out.println("时间:"+strTime);
}
timer.schedule(new LogTimerTask(),date,1000);
线程安全
安全问题概述
线程安全问题发生的三个条件:
1、资源共享
2、多线程并发
3、共享的资源可能被线程修改
主要体现在如果共享资源c正在被线程A获取后修改,在线程A修改完成前,线程B将c修改了,那线程A获取的资源c就已经过时,该资源未被同步,从而导致出错。
一般情况下局部变量一般不考虑线程安全问题。(引用变量可能有线程安全问题,基本数据变量不会有线程安全问题)
静态变量也可能存在线程安全问题,静态变量在堆中,堆是多线程共享的
下面是模拟多线程共享一个局部引用变量导致的线程安全问题:
public class security {
public static void main(String[] args){
Account account = new Account("y",1000);
Consumer consumer1 = new Consumer(account);
Consumer consumer2 = new Consumer(account);
Consumer consumer3 = new Consumer(account);
Consumer consumer4 = new Consumer(account);
Consumer consumer5 = new Consumer(account);
consumer1.start();
consumer2.start();
consumer3.start();
consumer4.start();
consumer5.start();
}
}
class Consumer extends Thread{
private Account account;
Consumer(Account account){
this.account = account;
}
@Override
public void run() {
account.get(100);
}
}
class Account{
private String name;
private double balance;
public Account(String name, double balance) {
this.name = name;
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void get(double money){
double balance = this.balance;
System.out.println("目前余额:"+balance);
setBalance(balance-money);
System.out.println("取走之后余额:"+this.getBalance());
}
}
结果出错
五个线程每次都对该资源进行了-100的操作,但是每个线程都显示剩下了900
若加上synchronized关键字的同步代码块
public void get(double money){
synchronized (this){
double balance = this.balance;
System.out.println("目前余额:"+balance);
setBalance(balance-money);
System.out.println("取走之后余额:"+this.getBalance());
}
}
正常执行
线程同步机制
线程同步机制就是解决线程安全问题的关键。实现同步原理是锁。尽量精准地放入需要共享的对象,要同步的对象越多,程序运行越慢。
死锁
假设线程A需要1和2两个资源,当前拥有1,而B也需要1和2,当前拥有2,两线程都将已经拥有的资源锁死了,未执行完该任务就不会释放对象锁,那么就会导致死锁发生。于是,要特别注意在嵌套使用synchronized代码块时一定要注意是否会产生死锁的问题。
线程通信
下面的代码想让线程A和线程B交替执行,这需要实现两线程间的通信
public class line {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t2.setName("t2");
t1.setName("t1");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
private int count = 0;
@Override
public void run() {
while (true){
if (count >= 100) break;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//count 会小于100
System.out.println(Thread.currentThread().getName()+"---->"+(++count));
}
}
}
未实现通信可能会出现下面这种情况
线程通信涉及三个方法,wait(),notify(),notifyAll() 这三个方法都是Object类的方法。
调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
例如:obj.wait(),obj是共享对象,当调用了wait方法后,会让obj对象上活跃的所有线程进入无限期等待。直到obj.notify()被调用,线程会被唤醒。obj.wait()方法调用后,会释放之前占用的对象锁。
注意!!!
上述方法只能在同步代码synchronized下实现,而且只能由共享对象来调用。
否则报错:
代码实现如下:
public class line {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t2.setName("t2");
t1.setName("t1");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
private int count = 0;
private Object object = new Object();
@Override
public void run() {
while (true){
synchronized (object){
//唤醒
object.notify();
if (count >= 100) break;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//count 会小于100
System.out.println(Thread.currentThread().getName()+"---->"+(++count));
try{
//进入睡眠
object.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
标签:java,Thread,void,提高效率,线程,println,new,多线程,public
From: https://blog.csdn.net/ddfhfjd/article/details/139531296