1.线程和进程
什么是线程和进程
进程就是一个应用程序,线程是一个进程中的执行场景/执行单元。
一个进程可以有多个线程。
对于java来说,在DOC命令下输入java HelloWorld并按下回车时,会启动一个JVM,这就是一个进程,JVM会启动一个主线程调用main方法,还会调用一个回收垃圾的线程,此时java程序中至少有两个线程并发。
进程A和进程B的资源不共享,但同一个进程中的线程C和线程D有共享的资源,也有各自的资源。
在内存中,不同的线程共用一份方法区和堆内存,而不同的线程会开辟不同的栈空间,事实上在内存中栈内存不仅仅有一份。
假设程序中有10个线程,就会有10个栈内存空间,每个进程之间互不干扰,每个栈内存之间互不干扰,比如main方法结束了,但程序还有可能没有结束,因为main方法结束只是意味着主栈空了,其他的栈还在压栈和弹栈,其他的进程还未结束。这就是并发机制,java中之所以有并发机制,就是为了提高程序的处理效率。
关于CPU
实际上单核CPU的设备并不能做到真正的多线程,我们之所以认为很多应用程序在同时进行,是因为CPU处理的速度极快,多个进程之间频繁切换。
2.创建线程
创建线程的第一种方式:继承Threah类
public class demo {
public static void main(String[] args) {
myThread t1 = new myThread();
// 直接调用run方法,等价于执行一个对象中的普通方法,不会创建一个新的线程
t1.run();
// 调用start方法,作用是创建一个新的栈内存,然后该方法结束
// 调用start方法之后,会在另一个线程中自动调用线程对象的run方法
// 而且run方法会在支栈的底部,就像main方法在主栈的底部一样。
t1.start();
// 注意:一定是执行了start方法之后才会执行main方法中之后的代码、,因为start方法在主栈中
//...
}
}
class myThread extends Thread{
@Override
public void run() {
// 在这里写新的线程需要执行的代码
}
}
创建线程的第二种方法:实现Runnable方法
public class demo {
public static void main(String[] args) {
myThread t = new myThread();
// 注意,线程的创建一定是由Thread类实例化产生的
Thread t1 = new Thread(t);
}
}
// 这还不是一个线程类,实例化它的对象也不能算是创建一个线程
class myThread implements Runnable{
@Override
public void run() {
// 在这里写新的线程需要执行的代码
}
}
同时我们可以使用匿名内部类的方式用第二种方式创建线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//...
}
});
要注意:我们更应该使用第二种方法创建线程,因为我们定义的实现接口的类可能还需要继承其他的类。
3.线程的五个状态
1.新建状态
2.死亡状态
3.就绪状态
4.运行状态
5.阻塞状态
4.线程中的几个方法
1.setName方法,修改线程的名字。
2.getName方法,获取线程的名字。
3.currentThread方法,获取当前的线程,这是一个静态的方法,返回值是Thread类型的。
public class demo {
public static void main(String[] args) {
myThread t = new myThread();
// setName方法
t.setName("线程x");
t.start();
}
}
class myThread extends Thread{
@Override
public void run() {
for(int i=0 ; i<100 ; i++){
// currentThread方法和getName方法
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
currentThread方法的一个说明
public class demo {
public static void main(String[] args) {
// 多态
Thread t = new myThread();
// 看看用对象名调用currentThread是什么结果
String s = t.currentThread().getName();
// 输出main
// 意味着currentThread方法和对象没有关系
System.out.println(s);
}
}
class myThread extends Thread{
@Override
public void run() {
for(int i=0 ; i<100 ; i++){
// currentThread方法和getName方法
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
4.sleep方法,用于让正在进行的线程进入阻塞状态,放弃占有的cpu时间片,让给其他线程使用。这是一个静态方法,参数是毫秒数。
public class demo {
public static void main(String[] args) {
Thread t = new myThread();
t.start();
}
}
class myThread extends Thread{
@Override
public void run() {
for(int i=0 ; i<5 ; i++){
// currentThread方法和getName方法
System.out.println(Thread.currentThread().getName()+"--->"+i);
// 注意sleep要处理异常,在而且只能用try catch,不能throws
// 因为Thread类没有抛出异常,子类不能比父类有更多更广的异常
try {
// 睡眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意,在main函数中如果写sleep线程,就算使用对象调用的形式,也不能让支线程休眠,只能让main函数休眠,因为sleep是静态方法。
5.interrupt方法,这个方法用于中断一个线程的睡眠,这是一个非静态方法。可以main函数里中断另一个线程的休眠。
注意:interrupt方法不是让线程进入运行状态,而是让线程进入就绪状态,被JVM调配。
public class demo {
public static void main(String[] args) {
Thread t = new myThread();
t.start();
//...
// 希望在前面的代码结束之后让t线程醒过来,把cpu时间片让给t
t.interrupt();
// 这种中断睡眠的方法依赖了java的异常处理机制
// 实际上会执行catch中的代码
}
}
class myThread extends Thread{
@Override
public void run() {
for(int i=0 ; i<5 ; i++){
try {
// 睡眠1秒
System.out.println("t线程正在执行");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
中断线程的一种方法:用一个属性就行了
public class demo {
public static void main(String[] args) {
myThread t = new myThread();
t.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 我想终止线程只需要把对象中的flat改成flase就可以了
t.flat = false;
}
}
class myThread extends Thread {
// 在我的类中定义一个boolean类型的属性
boolean flat = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 注意这个程序中判断要在for里,把判断放在for外没有意义,程序照常执行。
// 如果flat为真时,便让run方法继续执行
if (flat) {
try {
// 睡眠1秒
System.out.println("t线程正在执行");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("线程终止!");
return;
}
}
}
}
5.关于线程调度问题
在开发中有两种线程调度模型
1.抢占式调度模型:哪个线程的有限度高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型。
2.均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样,平均分配,一切平等,有一些编程语言的线程调度模型采用的就是这种方法。
java中提供了关于线程调度的一些方法
void setPriority (int newPriority),设置线程的优先级
int getPriority(),获取线程的优先级
最低优先级1,默认优先级5,最高优先级10
优先级比较高的获取CPU时间篇可能会多一些,但不完全是。
statuc void yield(),一个静态方法,让位方法,暂停当前正在执行的线程对象,并执行其他线程,yield方法的执行会让当前线程从“运行状态”回到“就绪状态”。
6.线程安全
前程安全是并发开发中的重点,因为我们的项目都运行在服务器中,关于线程对象的创建,线程的启动等,都已经实现完了。
重要的是,我们编写的程序要放在一个多线程的环境下,我们要关注的是一些数据在多线程并发的环境下是否是安全的。
当有多线程并发的环境,有共享数据的存在,而且共享数据有修改的行为。
怎样解决线程安全问题
使用线程同步机制,实际上就是让线程排队执行,不并发,就可以解决线程安全问题了。
线程排队会牺牲一部分效率,但此时会保证线程是安全的,数据的安全永远是前提。
异步编程机制:t1线程和t2线程各自执行各自的,实际上就是多线程并发。
同步变成机制:两个线程之间发生了等待关系,这就是同步编程模型。
重要案例一:银行取钱模型
// Account类
package Account;
public class Account {
private String ac;
private double money;
public Account(){
}
public Account (String ac,double money){
this.ac = ac;
this.money = money;
}
public String getAc() {
return ac;
}
public double getMoney() {
return money;
}
public void setAc(String ac) {
this.ac = ac;
}
public void setMoney(double money) {
this.money = money;
}
//取钱
public void withdraw(double money){
// 当前账户有多少前
double befor = this.money;
// 取之后剩余多少前
double after = befor - money;
try {
// 模拟网络延时,一定会出问题
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改setMoney
this.setMoney(after);
}
}
// myThread类
package Account;
public class myThread extends Thread{
private Account acc;
public myThread(Account acc){
this.acc = acc;
}
@Override
public void run() {
acc.withdraw(2500);
// 打印信息
System.out.println(Thread.currentThread().getName()+"取款2500,剩余"+acc.getMoney());
}
}
// 测试类
package Account;
public class test {
public static void main(String[] args) {
Account acc = new Account("账户1",5000);
myThread t1 = new myThread(acc);
myThread t2 = new myThread(acc);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
输出结果
t1取款2500,剩余2500.0
t2取款2500,剩余2500.0
java中使用锁机制保证了线程安全问题
public void withdraw(double money){
// 加上synchronized关键字
synchronized (this){
// 当前账户有多少前
double befor = this.money;
// 取之后剩余多少前
double after = befor - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改setMoney
this.setMoney(after);
}
}
synchronized后面小括号中传入的“数据”是相当关键,这个数据必须是多线程共享的数据,才能达到多线程排队的效果。
在我们的程序中括号里是this对象,而这个this对象是主类中的Account类的实例对象,只有一个,虽然我们创建了两个线程,当这两个线程共用一个Account对象,而一个Account对象只有一把锁,t1/t2占用了这把锁,就会让之后的线程排队。
实际上不一定必须要是this
可以是字符串”abc”,因为字符串在常量池中只有一个,但缺陷就是如果再实例化一个Account对象,那么两个对象就只有一把锁了。
Object obj = new Object(); // 如果在类中实例化一个对象,把这个对象放在小括号里也是可以的
// 因为内存中的堆只有一份,就算实例化了两个Account对象,他们只会指向堆中的一个obj地址,公用的。
关于锁池
关于变量的安全问题
实例变量:在堆中,堆在内存中只有一个,不同的线程操作堆时,就是并发操作,会存在安全问题。
静态变量:在方法区中,方法区中的静态变量也只有一个,和实例变量一样会存在安全问题。
局部变量:在栈中,永远都不会存在线程安全问题,因为局部变量不共享,一个线程一个栈,局部变量在栈中,所以局部变量永远都不会被共享。
public void withdraw(double money){
int i = 100;
i = 101;}
t1会修改t1栈中的i,t2会修改t2栈中的i,不存在线程安全问题。
在方法上也可以使用synchronized
public synchronized void withdraw(double money)
优点:代码简洁
缺点:此时锁的一定是this对象,不能是其他对象,这种方式不灵活。
举个例子
关于StringBuffer类和StringBuilder类,如果我们有一个局部变量中要用到StringBuffer类,不妨使用StringBuilder类,因为StringBuilder类,因为StringBuilder类虽然存在线程不安全问题,但是我们操作的局部变量一定不存在线程安全问题。
注:synchronized出现在静态方法上是找类锁。
死锁现象
死锁现象是由于对synchronized的使用不当,造成程序的死锁,这种错误不会出现异常,也不会出现错误,程序会一直僵持在那里,这种错误最难调试。
public class demo1 {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1,t2共用o1和o2对象
myThread1 t1 = new myThread1(o1,o2);
myThread1 t2 = new myThread1(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;
}
@Override
public void run() {
synchronized (o1){
try {
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;
}
@Override
public void run() {
synchronized (o2){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
涉及到了synchronized的嵌套就可能导致死锁现象,在开发中最好不要使用锁的嵌套使用。
在实际的开发中应该怎样解决线程安全问题
实际上一上来就使用synchronized选择线程的同步不是一个最好的选择,synchronized会使程序的执行效率变低,用户体验差,在不得已的情况下在选择线程同步机制。
第一种方案,尽量使用局部变量代替成员变量,在多线程开发中成员变量都在堆中,而堆只有一个,一定会涉及到数据的共享,使用局部变量,一个局部变量一个栈,使得数据不共享,不存在线程不安全问题,比如说在run方法中定义变量。
第二种方案,如果要使用实例变量/成员变量,可以考虑创建多个对象,不同对象的实例变量的内存不共享。比如说创造多个Account对象。
第三种方案,只能选择synchronized,使用线程同步机制。
7.守护线程
我们以上定义的线程都叫用户线程,实际上java语言中还有一类线程是守护线程,也叫做后台线程,具有代表性的线程就是垃圾回收线程,当用户线程/主线程(主线程main方法就是一个用户线程)结束时,守护线程自动结束。一般来说守护线程是一个死循环。
public class demo1 {
public static void main(String[] args) {
myThread t1 = new myThread();
t1.setName("守护线程");
// 我希望我的主线程的for循环结束之后,守护线程也结束,用到setDaemon方法
t1.setDaemon(true);
t1.start();
for (int i=1 ; i<10 ; i++){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"--->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class myThread extends Thread{
@Override
public void run() {
int i = 0;
// 守护线程是个死循环
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
8.生产者消费者模型
首先引入Object类中的两个有关线程的方法,wait方法和notify方法。这两个方法是java中任何一个对象都有的方法,他们不是通过线程调用的,而是通过普通方法调用的。
wait方法,表示让正在对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。会释放掉该线程之前占有的对象的锁。
notify方法,唤醒正在对象上等待的线程,还有一个notifyAll方法,唤醒在对象上处于等待的所有线程。只是通知,不会释放对象上之前占有的锁。
关于生产者消费者模型
生产者消费者模型模拟了一个这样的需求,
我们用list集合,假设在集合中只能存储1个元素,有1个元素就表示仓库满了,0个元素表示仓库空了,必须要做到这样的一个效果:生产1个消费1个。
// 主类
package ProducerandConsumer;
import java.util.LinkedList;
import java.util.List;
public class test {
public static void main(String[] args) {
// list模拟了一个仓库
List list = new LinkedList();
// 模拟了两个线程
Producer pro = new Producer(list);
Consumer con = new Consumer(list);
pro.setName("生产");
con.setName("消费");
pro.start();
con.start();
}
}
// 模拟了一个消费者类
package ProducerandConsumer;
import java.util.List;
public class Consumer extends Thread {
List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 让消费者一直消费
while (true) {
// 当消费的时候锁住list
synchronized (list) {
if (list.size() == 0) {
// 不能消费,等待生产线程
// 此时释放了锁等生产者生产
// 消费者线程停止
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明可以消费,开始消费
Object o = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + o);
// 通知生产者可以生产了,唤醒了list对象上的线程
list.notify();
}
}
}
}
// 生产者类
package ProducerandConsumer;
import java.util.List;
public class Producer extends Thread {
List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
if (list.size() > 0) {
// 不能生产,等待消费
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始生产
Object o = new Object();
list.add(o);
System.out.println(Thread.currentThread().getName() + "--->" + o);
list.notify();
}
}
}
}