//1-1(main 스레드 하나) public class NormalProcess { public static void main(String[] args) { long startTime = System.currentTimeMillis(); for(int i = 0; i < 300; i++) { System.out.println("-"); } System.out.print( "소요시간1:" + (System.currentTimeMillis() - startTime) ); for(int i = 0; i<300 ; i++) { System.out.println("|"); System.out.println( "소요시간2:" + (System.currentTimeMillis() - startTime) ); } } } ------------------------------------------------------------- //1-2(스레드 2개) class ThreadEx3 extends Thread{ public void run() { for(int i = 0 ; i <300; i++) { System.out.println("|"); } System.out.println( "소요시간2 : " + (System.currentTimeMillis() - UsingThreadProcess. startTime) ); } } public class UsingThreadProcess { static long startTime = 0; public static void main(String[] args) { ThreadEx3 th1 = new ThreadEx3(); ` startTime = System.currentTimeMillis(); th1.start(); for(int i= 0 ; i< 300; i++) { System.out.println("-"); } System.out.println( "소요시간1 :" + (System.currentTimeMillis() - UsingThreadProcess.startTime) ); } }1-1번코드와 1-2번코드를 비교해보면, 스레드를 사용한 것이 시간이 더 오래 걸리는 것을 알 수 있다.
시간만 가지고 확인해보면, 스레드한테 맡긴 것이계속하다보면 결국에는 스레드 사용하는 놈이 더 많은 시간을 쓴다.
입력창을 이용해서 스레드사용하는 것을 보면, 응답성같은 것들이 스레드를 사용함으로 인해서 계속적인 연속적인 작업이 가능했다. 사용자 입장에서 볼 때는 기다리지 않아도 되는 장점이 있다.
그러나 스레드를 사용하는 연산이 비용이 더 든다. 시간이 더 오래걸린다.대부분의 케이스에서 스레드 안쓰는 게 더 빠르다.
그럼 왜 스레드를 쓰는가? 사용자 편의를 위해서. 프로그램을 사용하는 사람(유저)들이 기다리는 것을 싫어한다.
그러나 스레드 쓰는 게 무조건 좋은 것은 아니다.

<단일스레드 모양>
여기서는 쓰기와 읽기 작업이 필요없다 그냥 한번에 쭉 하니까.

<멀티스레드 모양>
t1 : A1(150까지 읽고->쓰기작업(기록작성)) A2(읽기작업->151부터 시작)
위의 코드를 보면, 반복문을 돌면서 출력하고 있다.
만약 A1에서 150번까지 끝을 낸 후 B1-> A2로 가게 된다면,
A2쓰레드가 다시 자기 차례가 되었을 때 151번째부터 시작해야된다.
그럼 어딘가 이 정보, '내가 진행했던 작업이 어디까지 진행중인가'를 저장 해야 된다.
그래서 A1에서 쓰기작업(기록을 남겨 놓는 행위)을 하고, A2가 읽기작업을 한다.
쓰기작업 한 후,교대되면 다시 읽어와야 하고 계속 반복된다.
작업을 점점 많이할수록 증가할 수 밖에 없다. 그래서
스레드를 쓰는 형태가 더 느리다.
이렇게 시간이 더 거리더라도, 사람들이 기다리는 것을 싫어하니까 스레드는 쓴다. 사용자가 안쓰는 것은 결국 잘 안될 수 밖에 없다. 안쓰게되면 가치가 없어짐.
t1----->프로그램 흐름 하나 : context
t2----->프로그램 흐름 하나 : context
context switching(문맥교환)
: t1과 t2가 대기와 실행을 번갈아 가면서 하는 것.
t1과 t2를 왔다갔다할 때 생기는 비용 문맥교환비용.
public class NomalThreadTest { public static void main(String[] args) { //스레드 생성 Thread t = new Thread() { public void run() { try { //5초간 멈춤 Thread.sleep(5000); System.out.println("MyThread 종료"); }catch(Exception e) { //무시 } } }; //스레드시작 t.start(); //main메소드 종료 메시지 System.out.println("main() 종료"); } } //결과 main() 종료 ..(5초뒤)... MyThread 종료메인이 끝났는데 프로그램이 종료가 되지 않는다.
스레드가 5초 뒤에 깨고, MyTread가 끝나면 프로그램이 종료된다.
모든 스레드가 끝나야 프로그램이 종료된다.
public class DaemonThread { public static void main(String[] args) { Thread t = new Thread() { public void run() { try { System.out.println("MyThread 시작"); //5초간 멈춤 Thread.sleep(5000); //스레드 종료 메시지 System.out.println("MyThread 종료"); //얘가 안나온다. setDaemon() - 데몬스레드 }catch(Exception e) { } } }; //데몬 스레드로 설정 //반드시 start() 호출 전에 사용해야 한다!!!!! t.setDaemon(true); //스레드 시작 t.start(); //main메소드 종료 메시지 System.out.println("main() 종료"); } } //결과 main() 종료 MyThread 시작
<데몬스레드>
일반스레드들이 모두 죽으면 따라서 죽는 게 데몬스레드.
주 스레드의 작업을 돕는 보조적인 역할을 수행한다.
일반 스레드가 있고 데몬스레드가 있다. 일반은 앞에봤던 예시들.
앞에서는 메인스레드가 죽어도 시간을 다채우고 나왔는데, 데몬스레드는 걍 죽음.
언제 필요할까? 백그라운드작업을 할 때 쓰인다. 그 중에서도 사용자가 알수 없는 일을 처리할 때 스레드를 많이 쓰는데, 그때 데몬스레드를 사용한다.
메인이 출발하고 가비지콜랙터가 출발한다. 가비지 콜렉터가 데몬스레드형태를 띄고 있어서 가비지 콜렉터가 끝나지 않고 메인만 끝나도 프로그램이 끝나게 된다.
주 작업이 끝났을 때 같이 소멸해야할 일들이 있는데 그것을 데몬스레드로 만들어내면 된다. 아니면 프로그램이 끝나지 않을 수도 있다.
데몬스레드는 start() 전에 무조건 작성하기!!
class SomeThread extends Thread{ public SomeThread(String name) { super(name); } @Override public void run() { String name = this.getName(); for(int i = 0 ; i< 10 ; i++) { System.out.println(name + "is working"); try { Thread.sleep(500); }catch(InterruptedException e) { } } } } public class RunningTest { //main thread는 우선순위가 NORM_PRIORITY(5)이다. public static void main(String[] args) { SomeThread t1 = new SomeThread("A");//스레드 이름을 설정할 수 있음. SomeThread t2 = new SomeThread("B"); SomeThread t3 = new SomeThread("C"); ] t1.setPriority(Thread.MIN_PRIORITY); t2.setPriority(Thread.NORM_PRIORITY); t3.setPriority(Thread.MAX_PRIORITY); ` System.out.println(t1.getPriority()); System.out.println(t2.getPriority()); System.out.println(t3.getPriority()); ` t1.start(); t2.start(); t3.start(); } }
<우선순위>
쓰레드의 우선순위 설정 - 동일한 조건에서 경쟁한다고 쳤을 때 C가 먼저 끝날 경우가 크다. 경쟁을 할 때 가상머신이 어떤 쓰레드를 선택할 것인가에 관한 우선순위.
우선순위가 약간 프로그램에 영향을 준다.
위의 코드를 보면 C가 가장 우선순위가 높다.
C가 끝에 나올수도 있다. 그건 확률의 문제다.
그러나 엄청 많이 해보면 결국엔 C가 먼저 끝날 확률이 훨씬 높다.
스레드 수가 많아졌을 때, 내가 컨트롤 해야되는 게 100개인데, 우선순위를 내가 막 촘촘하게 만들었지만 해결되지 않는다.
왜냐면 모든 경우의 수를 사람이 커버할 수 없기 때문이다.
내가 생각한 것과 다르게 움직일 수 있다.
우선순위에 100프로 의존하겠끔 만들어 놓지 않았다.
우리가 미리 어느 타이밍에 끝난다고 예측할 수 없다.
그래서 우선순위는 절대적이지 않다.
만약 갑자기 미리 정해져 있는 우선순위를 가지고 있는 3개보다 우선순위가 높은 게 나타난다면? -영원히 선택받지 못하는 스레드가 발생할 수 도 있다.
우선순위가 높다고 해서 100퍼센트 빠르게 나온다고 할 수 없다. 확률만 높아짐.
최대 우선순위(MAX_PRIORITY) : 10
보통 우선순위(NORM_PRIORITY) : 5
최소 우선순위(MIN_PRIORITY) : 1
나를 생성해낸 우선순위를 그대로 들고온다. 여기서는 main이 얘네를 생성해냈다. 그래서 main의 우선순위가 5이면 t1,t2,t3도 우선순위도 5이다.
⭐결론 : 스레드 우선순위를 바꾸지말라고 얘기한다(건들지마라). 내가 차등을 주는 순간, 내가 생각했던 것이랑 완전히 다르게 작동될 수 있다.
public class ThreadJoinTestB { public static void main(String[] args) { Thread t = new Thread() { public void run() { try { //2초간 멈춤 Thread.sleep(2000); System.out.println("MyTread 종료"); //3초간 멈춤 Thread.sleep(3000); }catch(InterruptedException e) { //sleep()써서 이 예외처리 함. } } }; //스레드 시작 t.start(); try { //join()메소드 실행 //t스레드가 종료될 때까지 main스레드가 기다림. ⭐t.join();⭐ }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("main() 종료"); } } //결과 MyTread 종료 ...2초, 3초... main() 종료
t스레드가 종료될 때까지 main스레드가 기다림.
다른 스레드가 모두 종료될 때까지 기다렸다가 꺼짐.
t.join()에서 t는 영향을 아무것도 안받고, join()이라는 메소드를 실행한 스레드가 영향을 받는다.
앞의 예시에서는 t가 다 영향을 받았다.(?)
join()이라는 메소드는 스레드가 2초, 3초가 끝나면 main이 끝이나게 된다.
소유자가 객체가 영향을 받는 게 아니고 실행한 객체들이 영향을 받는다.
InterruptedException
: 실행(run)하고 있다가 실행불가상태(not runnable)로 가는 메소드들이 있다.
: sleep(), join() 등이 InterruptedException을 쓴다.
<join()예시>
class ThreadEx13_1 extends Thread{ //스레드1 public void run() { for(int i = 0 ; i < 300 ; i++) { System.out.println("-"); } }//run() } class ThreadEx13_2 extends Thread { //스레드2 public void run() { for(int i = 0; i < 300 ; i++) { System.out.println("|"); } }//run() } public class ThreadEx13 { static long startTime = 0; public static void main(String[] args) { ThreadEx13_1 th1 = new ThreadEx13_1(); ThreadEx13_2 th2 = new ThreadEx13_2(); th1.start(); th2.start(); startTime = System.currentTimeMillis(); // try { // th1.join(); // th2.join(); // // }catch(InterruptedException e) { // // } 💡 위의 코드처럼 함께 넣어 놓는 게 아니라 아래 코드처럼 따로따로 담아두기. 위의 코드처럼 했다가는 th1기다리고 있다가 th2가 죽으면 무시되고 지나가게 되어 버리게 된다. 밑에 처럼 따로 만들면 둘 다 기다리게 된다. 💡 try { th1.join(); }catch(InterruptedException e) { } try { th2.join(); }catch(InterruptedException e) { } System.out.println("소요시간 : " + (System.currentTimeMillis()- ThreadEx13.startTime)); }//main }은행에서도 비밀번호를 쳐야 그 다음단계를 할 수 있듯이, 앞에 것을 완성 해야 뒤에 것을 사용할 수 있도록 해야하는 경우가 있는데, 그걸 join()을 활용.
class UnsafeAccount { private int balance; public int getBalance() { return balance; } public void deposit(int val) { //잔액을 증가시키는 메소드 balance += val; } //잔액500원일때 public void withdraw(int val) { if(balance >= val) { //잔고가 크거나 같을때만 금액감소시킨다. try { Thread.sleep(500); }catch(InterruptedException e) { } balance -= val; } System.out.println( "name: "+ Thread.currentThread().getName() + ", balance = " + getBalance() ); } } public class UnsafeAccountSample { public static void main(String[] args) { //공유자원 final UnsafeAccount account = new UnsafeAccount(); //final붙은 이유는? 1.8에는 이게 없어도 된다. 1.7까지는 지역변수를 참조할 수 없다. 그래서 붙여야한다. account.deposit(5000); Runnable withdrawRun = new Runnable() { //10번돌면서 account를 500원씩 빼는거. public void run() { for(int i =0 ; i < 10 ; i++) { account.withdraw(500); } } }; Thread t1 = new Thread(withdrawRun); Thread t2 = new Thread(withdrawRun); //1도 account에서 500출금하고, 2도 account에서 500원출금한다. 이게 바로 공유자원. 1,2가 같이 쓰는 객체를 공유자원이라고 한다. //객체하나를 여러개 스레드가 같이 쓰는 걸 공유자원이라고 한다. t1.start(); t2.start(); } } /*결과 * name: Thread-0, balance = 4500 name: Thread-1, balance = 4500 name: Thread-1, balance = 3500 name: Thread-0, balance = 3500 name: Thread-1, balance = 3000 name: Thread-0, balance = 2500 name: Thread-0, balance = 2000 name: Thread-1, balance = 1500 name: Thread-1, balance = 500 name: Thread-0, balance = 500 name: Thread-1, balance = 0 name: Thread-1, balance = -500 name: Thread-0, balance = -500 name: Thread-0, balance = -500 name: Thread-0, balance = -500 name: Thread-0, balance = -500 name: Thread-0, balance = -500 name: Thread-1, balance = -500 name: Thread-1, balance = -500 name: Thread-1, balance = -500*/
스레드1도 account에서 500출금하고, 스레드2도 account에서 500원출금한다. 이게 바로 공유자원.
스레드1,스레드2가 같이 쓰는 객체를 공유자원이라고 한다.
추가) 공유자원은 synchronized 블럭을 사용하는 자원을 말하는 것 같음.
공유자원은 2개이상의 스레드가 진입을 하는 곳을 말하는데, 예를 들어 회원가입할 때 a라는 아이디를 만들고 있었는데 동시에 다른 사람도 a라는 아이디를 만든다면 중복되는 일이 일어난다. 이걸 방지하기 위해서 synchronized를 쓰면 중복되는 일이 일어나지 않는다.
스레드1과 스레드2가 동시에 진입할 수 있는 영역. 여러 개의 스레드가 동시에 진입할 수 있는 곳.
위의 문제를 해결할 방법은? 문을 만들어서 안에 쓰던 사람이 나올 때까지 사용하는 동안 문을 닫아놓으면 된다. 이런상황을 '상호배제'라고 한다.
내생각) 쓰레드 상에서 동기화가 깨진 것 중에 한 예시인듯.
똑같은 대상을 알고 동일한 정보를 공유한다
= 동기화되었다.동기화가 이루어졌다.동기화가 만족한다.
똑같은 대상을 알고 있지만 다르게 알고 있다
= 동기화가 깨졌다.
위의 코드처럼 '-500'이라는 값이 안나오도록 하려면?
동기화시키면 된다.
class SyncAccount { private int balance; public synchronized int getBalance() { return balance; } public synchronized void deposit(int val) { balance += val; } public synchronized void withdraw(int val) { if(balance >= val) { try { 🔥Thread.sleep(500); }catch(InterruptedException e) { } balance -= val; } System.out.println( "name : " + Thread.currentThread().getName() + ", balance=" + this.getBalance() ); } } public class SyncAccountSample { public static void main(String[] args) { final SyncAccount account = new SyncAccount(); account.deposit(5000); ` Runnable withdrawRun = new Runnable() { @Override public void run() { for(int i = 0 ; i<10 ; i++) { account.withdraw(500); } } }; Thread t1 = new Thread(withdrawRun); Thread t2 = new Thread(withdrawRun); ` t1.start(); t2.start(); } } /*결과 * 하나의 스레드가 작업할 동안 다른 스레드가 개입을 안하고 있다. name : Thread-0, balance=4500 name : Thread-0, balance=4000 name : Thread-0, balance=3500 name : Thread-0, balance=3000 name : Thread-0, balance=2500 name : Thread-0, balance=2000 name : Thread-0, balance=1500 name : Thread-0, balance=1000 name : Thread-0, balance=500 name : Thread-0, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 name : Thread-1, balance=0 */
synchronized가 붙은 메소드 - 상호배제를 지원하는 메소드.
스레드가 여러개 들어올 수 없다. 일을 끝날 때까지 기다려야 한다.
자바의 모든 객체는 'lock'이라는 정보를 하나씩 가지고 태어난다.
SyncAccount()의 lock를 들고 들어간다. 객체마다 락은 하나밖에없기때문에, lock이 없다면 누가 그 lock을 들고 저안에 들어가있다는 뜻이다.
'lock'은 '열쇠'같은 거라고 생각하면 편하다.
만약 열쇠를 들고 안으로 들어가서 문을 잠근다고 하면, 다른 애가 들어가려면 열쇠가 필요함.
언제 열쇠를 푼다? 먼저 들어간 것이 withdraw()가 끝나면 열쇠를 다시 돌려준다.
위에서는 synchronized가 붙은 메소드가 3개다.
(getBalance(), deposit(),withdraw())
⭐synchronized가 붙은 이 3개의 메소드는 this의 lock이라는 단 한개의 열쇠로만 들어간다.
어떤 스레드가 deposit()에 들어간 채로 멈춰있다면 다른 스레드가 다른 메소드에 들어가지 못한다.
`
만약 getBalance()에 먼저 A가 들어가있고, 조금 후에 deposit()에 B에 들어갈 수도 있지 않느냐?라고 생각할 수도 있는데,
객체의 '상태'에 접근하는 메소드라서 동시에 접근하지 않더라도 문제가 똑같이 발생할 수 있다.
밸런스값을 3개 중에 하나의 메소드에 들어가서 바꿀 수 있다.
그래서 열쇠(lock)를 3개의 메소드를 합쳐서 1개만 들고 있다.
synchronized메소드는 lock의 주체를 어떤 객체의 lock을 사용한다?
this의 락을 사용한다.
💡위의 코드에서는 final SyncAccount account = new SyncAccount(); 에서의 'account'이다.
위의 코드에서 🔥부분을 보면, thread.sleep()되어 있다.
여기서 t1이 lock을 들고 슬립상태로 빠진다. t1이 lock을 들고 있기 때문에 t2가 runable->run에 갔다가 blocked된다. lock을 취득하지 못한 스레드는 blocked상태에 빠진다.
t1과 t2 모두 (not runnable)로 가게 되고, 1번이 자고 있는 시간동안 2번도 계속 자게 된다.
락을 가진 1번스레드가 돌아오면은 2번스레드도 다시 준비상태로 돌아온다.
근데 여기서 모든 스레드가 갖혀 잇는 상태를 'deadlock'(걍 멈춰있는 상태)이라고 한다.
모든 스레드가 전부 다 불가상태에 빠져버린거 프로그램이 종료가 되지 않앗는데 거기 들어가잇는 걸 풀어줄 스레드가 없는 것.

위의 사진에서
Blocked - 락이 없이 갈 때도, 입출력 - 빠져나올 수 잇는 조건이 있음(무조건 빠져나온다는 것은 없지만.)
timed waiting - 시간제한이 있는 기다림(sleep, join(언제까지만 기다려라고 지정할 수 있음))
waiting - 웨이팅은 시간조건이 없는 케이스이다. 데드락(실행되고 있는 게 없다. 일을 해야하는 스레드들이 일을 못하는 곳에 가있음.)같은 거 만들어 내는 주범이 얘가 될 가능성이 높다.
: 스레드의 상태변화가 없이 갇혀있는게 데드락이라면, 라이브락은 스레드의 상태가 변화가 생기는데 일은 안되는것이다.
라이브락은 스레드끼리의 상태변화가 생긱는데 일이 안일어난다(식사를 먹어야 되는데,숫가락,접시필요).
공통점은 둘다 일이 안되는거.
<기아상태>
영원히 자원을 받지 못하는 거.
public class SynchVsNotCynch { private static final long CALL_COUNT = 100000000L; public static void main(String[] args) { ` trial(CALL_COUNT, new NotSynch()); trial(CALL_COUNT, new Synch()); } private static void trial(long count, Object obj) { String msg = obj.toString(); System.out.println(msg + "BEGIN"); long stratTime = System.currentTimeMillis(); for(int i = 0 ; i < count ; i++) { obj.toString(); } System.out.println( "Elapsed time = " + (System.currentTimeMillis() - stratTime)+ "ms" ); System.out.println(msg + ": END"); } } class Synch { private final String name = "Synch"; @Override public synchronized String toString() { //synchronized O //싱글스레드 상태에서도 성능차이가 난다. return "[" + name + "]"; } } class NotSynch extends Synch { private final String name = "NotSych"; @Override public String toString() { //synchronized X return "[" + name + "]"; } } //결과 [NotSych]BEGIN Elapsed time = 32ms NotSych]: END [Synch]BEGIN Elapsed time = 1852ms
thread'safe' - 동기화처리가 되어있다. 스트링버퍼, 해쉬테이블, 벡터. 원소의 동기화가 아니라 add()라고 쓸 때 동기화가 되어 있는 거.
원소를 두개의 스레드가 꺼내서 원소처리할 때는 다시 동기화처리 해주ㅓ야 한다.
thread'notsafe' - 스윙컴포넌트, 스트링빌더, 해쉬맵, 어레이리스트.
synchronized(동기화)가 공짜가 아니다. 시간이 더 오래 걸린다.
싱크로나이즈드가 프로그램성능에 굉장히 데미지를 줄 수 있다.
동시에 막 가다가 싱크로 만나면 하나만 가고 하나는 기다려야 되는데
이때 '병목현상'이 일어남.
💡synchronized가 병목현상이 나타날 수 밖에 없이 만들어졌다.