1.在学习多线程之前的补充
主流的IDEA编译器不同于Eclipse,我们需要了解其一部分快捷键
- a:try catch快捷键:选中不止一行的代码,ctrl+alt+t 双击try-catch;
- b:在ide中双击shift后输入关键字可以查看源码;
- c: 在源码中查找方法快捷键Ctrl+F12;
- d:如果想要查看方法细节可以按住Ctrl点击方法;
- e:若果想要回到原来位置Ctrl+alt+方向左键;
- f:IDEA自动导包要手动 Alt + Enter 进行导入的,也可以进行抛异常。
2.线程和进程区别
- a:进程是程序的执行过程,具有动态性,即运行的程序就叫进程,不运行就叫程序
- b:线程系统中的最小执行单元,同一进程中有多个线程,线程可以共享资源,一旦出现共享
- 资源,必须注意线程安全!
3.线程分类
分为两种:一类是守护线程,典型是垃圾回收GC;第二类是用户线程,当JVM中都是JVM守护线程,那么当前的JVM将退出。
4.线程继承的两种方式
4.1继承Thread类
步骤为:
- 1.创建的线程类继承Thread类,
- 2.重写其中的run方法,若在主类中调用线程的话,则 new出线程类,调用它的start方法即可,这里的start方法完成了两个工作:启动子线程,调用 子线程的run方法;
//线程类
class TestThread extends Thread {
@Override
public void run() {
for(int i=1;i<=100;i++){
if(i%2==0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/*主类中调用:*/
TestThread myThread = new TestThread();
TestThread2 myThread2 = new TestThread2();
myThread.setName("偶数线程");//为线程命名
myThread2.setName("奇数线程");
myThread.start();//启动线程
myThread2.start();
Thread.currentThread().setName("主线程");//Thread.currentThread()表示当前的线程
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+ ":" + i);
}
4.2实现Runnable接口
即创建的线程类实现Runnable接口,实现其中的run方法,若在主类中调用线程的话,则new出线程类,这里注意不能直接调用他的start方法,因为通过快捷键查到源码发现没有start方法,而是通过Thread 线程名 = new Thread(new 出来的类),然后调用这个线程的start的方法才能启动线程,反之线程的启动该必须调用start方法!
class TestThread2 implements Runnable{
public void run() {
for(int i=1;i<=100;i++){
if(i%2!=0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/*然后调用时一定注意,不是直接调用start方法,*/
TestThread2 myThread2 = new TestThread2();
Thread t2 = new Thread(myThread2);
t2.setName("奇数线程");//为线程命名
t2.start();//启动线程
Thread.currentThread().setName("主线程");//Thread.currentThread()表示当前的线程
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+ ":" + i);
}
5.线程中的几个常用的方法以及其生命周期
- 1:t.yield(),会使t线程释放当前cpu资源,但如果有锁的话他不会释放锁;
- 2:t.join(),使当前的线程停下来,启动并执行t线程,只有当t线程完全执行完后,当前线程才可以继续执行
- 3.t.sleep(i),显示的是让线程睡眠i毫秒,sleep方法执行期间不会释放他的锁;
- 4.线程通信过程中的几种方法:wait(),notify(),notifyAll(),通常他们前面不需要加线程名,表示直接对同步方法或者同步块生效;
- 5.线程的生命周期:新建,就绪,运行,阻塞,死亡;
运行状态下经过sleep,等待同步锁,wait()/join()方法可以进入阻塞状态,然后通过sleep时间到,获取同步锁,notify()/notifyAll()回到就绪状态。而且这三者都是一一对应的。
6.线程安全问题(重点!)
6.1线程安全问题的出现危机:
线程安全一般出现在多线程中,而且基本只要出现共享资源时都会出现线程安全的隐患,关键在于多线程对共享资源的操作会导致这个问题,比如窗口售票的问题:
/**
* 模拟火车票多窗口售票流程,总共100张票
* 该程序存在线程安全的问题
* @author Weiguo Liu
*
*/
class Window1 extends Thread{
//将变量声明为static属性,则所有new出来的该类的对象共用该属性,
//这里声明为三个线程,那他们共用ticket属性,并且该线程的ticket的值是从上一个线程获取的,不会每次都对其进行初始化
static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket>0){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}else{
break;
}
}
}
}
public class TicketsThread {
public static void main(String[] args) {
Window1 thread1 = new Window1();
Window1 thread2 = new Window1();
Window1 thread3 = new Window1();
thread1.setName("1号窗口");
thread2.setName("2号窗口");
thread3.setName("3号窗口");
thread1.start();
thread2.start();
thread3.start();
}
}
这里开启了3个窗口同时出售100张票,这里注意共享资源的声明方式,在extends Thread的方法中,由于每次创建线程Thread类的时候都是new一个新的对象,所以在线程类中对ticket 的声明必须加上static才能使多个同一类的对象公用ticket这一属性,否则每次创建线程的时候ticket都会初始化为100,即三个窗口最后卖出不是100张票而是300张票,static可以使同一类的对象,只要是同一类的对象,都会公用ticket属性,上一个线程对ticket作的加加减减的结果会另同一类的线程继续使用,而不是初始化100再用;但是在implements Runnale的线程类中,则不需要这么干,因为他们都是同一个对象,只是线程不一样,比如这个线程类是用的implements Runnable方式,那她在创建线程是这么做的:
Window1 thread1 = new Window1();
Thread t1 = new Thread(thread1);//用的是thread1
Thread t2 = new Thread (thread1);//还是用的thread1
所以这里他们创建子线程的时候用的是同一类对象,在线程类中就不必对共享资源加static静态处理。关于线程安全的问题,可以站在一个极端的角度来看,这里的sleep()只是纯粹为了放大这个线程中的安全隐患,并没实际意义,比如只剩最后一张票的时候,这时候1号窗口线程刚好判断了还有一张票(即进入了if(ticket>0)这个条件的内部),将要打印还未打印的时候,这时候票数还没有进行减1变0,这时候,2号窗口的线程发现票数为1也进来了,这时候两个线程在进行打印减1的过程中,必定一前一后,那么先打印的没有问题,但是这时候票数变成了0,后面的线程在打印的时候变成0号票,这就造成了线程安全的问题。
6.2线程安全问题的解决
线程安全问题的原因归根是同一时刻有多个线程对共享资源进行操作而引起的,在这里可以考虑让同一时刻对共享数据操作的线程减少为有且只有一个便可以解决这个问题,这也就是java中线程同步机制,两种发法:同步代码块和同步方法。
6.2.1同步代码块
在对共享数据进行操作的代码块中加上synchronized(同步监视器,即锁){//需要被同步的代码,即对共享资源进行操作的代码块},这里的同步监视器可以是任何一个对象来充当甚至直接Object obj = new Object();后用obj来充当,但是关于同步锁的申明也必须申明为共享资源,既不能在run()方法中申明,
这里继承和实现又有所不同,继承的方法中对于同步锁的声明必须写成static Object obj = new Object()的形式,但是实现的方式可以写成Object obj = new Object();更可以不用定义,直接用当前对象this来作为同步锁,但继承方式的线程不能用this;
//这是继承的方式,所以只能用这种方式
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket>0){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}else{
break;
}
}
}
}
/*1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23*/
//再来看实现的方式
//这里是线程采用的是实现的方式(implements Runnable),同步锁不必声明可以直接用this,当然也可声明Object obj = new Object(),然后用obj来充当同步锁
@Override
public void run() {
while(true){
synchronized(this){
if(ticket>0){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}else{
break;
}
}
}
}
加上同步锁后,同一时间只允许一个线程对System.out.println(Thread.currentThread().getName() + “:” + ticket–);进行执行,所以不会在出现0号票或者-1号票的线程安全问题。
6.2.2同步方法(只有实现方式的线程可以用,继承方式的线程不可用)
同步方法就是在一个方法上加上锁,并且锁默认的是this,不能为其他,这也是为什么只能实现方式的线程类可以用,而继承方式的线程不可以用,这里通常是把对共享资源的操作单独拿出来封装在一个方法中,然后对这个方法加上锁同步,例如:
class Window implements Runnable{
//将变量声明为static属性,则所有new出来的该类的对象共用该属性,
//这里声明为三个线程,那他们共用ticket属性,并且该线程的ticket的值是从上一个线程获取的,不会每次都对其进行初始化
static int ticket = 100;
static Object obj = new Object();
@Override
public void run() {
while(true){
operation();
}
}
//对operation这个方法进行同步,这个方法同一时刻只允许一个线程执行
public synchronized void operation(){
if(ticket>0){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}
}
}
6.3死锁问题
产生:不同线程分别占用对方需要同步的资源,都在等待双方放弃同步资源,从而导致死锁,如下:
/**
* 测试死锁的 产生
* 双方都在等待对方手里的锁
* @author Weiguo Liu
*
*/
public class DeadLockTest {
static StringBuffer sb1 = new StringBuffer();
static StringBuffer sb2 = new StringBuffer();
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
synchronized (sb1) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
sb1.append("A");
synchronized (sb2) {
sb2.append("B");
System.out.println("sb1:" + sb1);
System.out.println("sb2:" + sb2);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized(sb2){
sb1.append("C");
synchronized(sb1){
sb2.append("D");
System.out.println("sb1:" + sb1);
System.out.println("sb2:" + sb2);
}
}
}
}.start();
}
}
前一个线程首先拿到了sb1锁,然后sleep()一段时间后,sb2锁被第二个线程拿到了,第一个线程在等sb2锁,第二个线程在等sb1锁,两个线程都在等对方的锁,所以一直处在等待的状态,形成死锁。
这里对锁的释放是否做一个总结:
释放锁的操作:1.同步结束2.同步过程中遇见未处理的错误或者异常或者break等3.同步过程中执行了wait()方法,当前线程将会挂起,释放锁
不释放锁的操作:1.同步过程中调用了sleep()或者yield()方法 2.其他线程调用了该线程的suspend()方法
7.线程通信
线程通信中的几个常用方法:
- 1.wait(),是当前线程挂起并放弃cpu资源,使其他线程可以访问共享资源,而当前执行的wait()线程重新排队等待资源访问的机会;
- 2.notify(),唤醒正在排队中优先级最高的线程,结束等待
- 3.notifyAll(),唤醒所有正在排队的线程,这种经常用在交替对共享资源进行操作的地方。比如:两个线程交替打印1到100;
while (true) {
synchronized(this){
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + (i++));
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else{
break;
}
}
}
最后是关于线程的消费者生产者的经典例子:
/**
* 生产者/消费者的问题
* 生产者将产品交给店员(Clerk),消费者从店员处取走产品,但店员一次只能持有固定数量的
* 产品,比如20,如果生产者试图生产更多的产品,店员将通知生产者停下,因为店里空间只能放20;
* 如果店中有产品了,店员将通知消费者来取
* @author Weiguo Liu
*
*/
//生产者
class Producer implements Runnable {
Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("生产者开始生产产品");
while(true){
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clerk.addProduct();
}
}
}
//店员
class Clerk {
//产品数量
public int product;
public synchronized void addProduct(){
//生产产品
if(product>=20){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
product++;
System.out.println(Thread.currentThread().getName() + "生产了第" + product + "件产品");
notifyAll();
}
}
public synchronized void consumeProduct(){
if(product <= 0){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
System.out.println(Thread.currentThread().getName() + "消费了第" + product + "件产品");
product--;
notifyAll();
}
}
}
//消费者
class Consumer implements Runnable {
Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("消费者消费产品");
while(true){
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProducerAndCustomer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Thread t1 = new Thread(p1);
Thread t2 = new Thread(c1);
t1.setName("生产者");
t2.setName("消费者");
t1.start();
t2.start();
}
}
8.多线程的应用